home *** CD-ROM | disk | FTP | other *** search
/ HPAVC / HPAVC CD-ROM.iso / WINER.ZIP / CHAP6.TXT < prev    next >
Text File  |  1994-09-04  |  186KB  |  3,806 lines

  1.                                  CHAPTER 6
  2.  
  3.                          FILE AND DEVICE HANDLING
  4.  
  5.  
  6.      At some point, all but the most trivial computer programs will need to
  7. store and retrieve data using a disk file.  Data files are used for two
  8. primary purposes: to hold information when there is more than can fit into
  9. the computer's memory all at once, and to provide a permanent, non-volatile
  10. means of storage.  Files are also used to allow data from one computer to
  11. be used on another.  Such data sharing can be as simple as a "sneaker net"
  12. system, whereby a floppy disk is manually carried from one PC to another,
  13. or as complex as a multi-user network where disk data can be accessed
  14. simultaneously by several users.
  15.      Although there are two fundamentally different types of disk drives,
  16. floppy and fixed [not counting CD-ROMs drives which are removable], they
  17. are accessed identically using the same BASIC statements.  BASIC's file
  18. commands may also be used to communicate with devices such as a printer or
  19. modem, and even the screen and keyboard.  There are many ways to manipulate
  20. files and devices, and some are substantially faster than others.  By
  21. understanding fully how BASIC interacts with DOS, file access in your
  22. programs can often be speeded up by a factor of five or even more.
  23.      In this chapter I will address the fundamental aspects of file and
  24. device handling, and provide specific examples of how to achieve the
  25. highest performance possible.  I will begin with an overview of how DOS
  26. organizes information on a disk, and then continue with practical examples. 
  27. Unlike earlier chapters in which only short program fragments were shown,
  28. several complete programs and subprograms will be presented to illustrate
  29. the most important of these techniques in context.  I will also describe
  30. the underlying theory of how disks are organized, and explain why this is
  31. important for the BASIC programmer to know.
  32.      In Chapter 7 the subject of files will be continued; there you will
  33. learn how to write programs for use with a network, and also how relational
  34. databases are constructed.  In particular, coverage of these two very
  35. important subjects is severely lacking in the documentation that comes with
  36. Microsoft BASIC.  As personal computers continue to permeate the office
  37. environment, networks and databases are becoming ever more common.  Many
  38. programmers find themselves in the awkward position of having to write
  39. programs that run on a network, but with no adequate source of information.
  40.  
  41.  
  42. DISK FILE FUNDAMENTALS
  43. ======================
  44.  
  45. All disks used with MS-DOS are organized into groups of bytes called
  46. *sectors*, and these sectors are further combined into *clusters*.  DOS
  47. keeps track of every file on a disk, but with this organization DOS needs
  48. to remember only the cluster number at which each file begins.  The minimum
  49. amount of disk space that is allocated by DOS is one cluster.  Therefore,
  50. if you create a very small file--say, ten bytes--an entire cluster is
  51. allocated to that file, and then marked as unavailable for other use.
  52.      In most cases, each disk sector holds 512 bytes; however, one
  53. exception is when you use a RAM disk to simulate a disk drive in memory. 
  54. Many RAM disk programs lets you specify a smaller sector size, to minimize
  55. waste when there are many small files.  The number of sectors that are
  56. stored in each cluster depends on the type of disk and its size.  For
  57. example, a 360K floppy disk stores two sectors in each cluster, and a 32 MB
  58. hard disk formatted using DOS 3.3 stores four sectors in each cluster. 
  59. Therefore, the minimum unit of storage allocation for these disks is 1K
  60. (1024 bytes), and 2K (2048 bytes) respectively.  DOS 2.x offers less room
  61. to store cluster numbers, and must combine more sectors into each cluster. 
  62. A 20MB hard disk formatted with DOS 2.1 allocates 8K for even a one-line
  63. batch file!
  64.      As files are created and appended, DOS allocates new space to hold the
  65. file contents.  By allocating disk space in units, DOS is also able to
  66. minimize disk fragmentation.  As you learned in Chapter 2, BASIC manages
  67. variable-length strings by claiming new memory as necessary.  When
  68. available memory is exhausted BASIC compacts its string space, overwriting
  69. abandoned string data with strings that are still active.
  70.      This method is not practical with disk files, because copying data
  71. from one part of the disk to another for the purpose of compaction would
  72. take an unacceptable amount of time.  Therefore, DOS initially allocates an
  73. entire cluster for each file, to provide space for subsequent data.  When
  74. the ten-byte file mentioned earlier is added to, space on the disk has
  75. already been set aside for all or part of the new data that will be
  76. written.  And when the first cluster's capacity is exceeded, DOS allocates
  77. an entire second cluster to hold the additional data.
  78.      Even though it is common for a disk to become fragmented, allocating
  79. clusters that are comprised of groups of contiguous sectors greatly reduces
  80. the number of individual fragments that must be accessed.  The track,
  81. sector, and cluster makeup of a 360k 5-1/4 inch floppy disk is shown in
  82. Figure 6-1.
  83.  
  84.  
  85. Figure 6.1: Sector and cluster organization for a 360k floppy disk. 
  86. [Sorry, this figure is not available.]
  87.  
  88.  
  89. This disk is divided into 40 circular tracks, and each track is further
  90. divided into nine sectors.  One track holds 512 bytes, and each pair of
  91. tracks is combined to form a single cluster.  For a 360k disk, no file
  92. fragment will ever be smaller than two clusters, since this is the minimum
  93. amount of space that DOS allocates.  Likewise, a hard disk that combines
  94. four sectors into each cluster will never be divided into pieces smaller
  95. than four sectors.
  96.      Please understand that tracks and sectors are physical entities that
  97. are magnetically encoded onto the disk when it is formatted--it is DOS that
  98. treats each pair of sectors as a single cluster.  Note that since a 360k
  99. disk stores nine sectors on each track, some clusters will in fact span two
  100. tracks.
  101.      Using the disk in Figure 6-1 as an example, the first short file that
  102. is written to it will be placed in cluster 1 (sectors 1 and 2), even if the
  103. file does not fill both sectors.  The second file written to this disk will
  104. then be stored starting at cluster 2 (sectors 3 and 4).  If the first file
  105. is later extended beyond the 1,024 bytes that can fit into cluster 1, the
  106. excess will be added beginning at cluster 3 (sectors 5 and 6).  Thus, when
  107. DOS reads the first file sequentially, it must read cluster 1, skip over
  108. cluster 2, and then continue reading at cluster 3.
  109.      Of course, this takes longer than reading a file that is contiguous,
  110. because the disk drive must wait until the second file's intervening
  111. sectors have passed beneath it.  This problem is compounded by additional
  112. head movement when the fragmentation extends across more than one track, as
  113. well as by other timing issues.
  114.      There are also three special areas on every disk: the boot sector, the
  115. Disk Directory and the File Allocation Table (FAT).  DOS uses the directory
  116. and FAT to know the name of each file, and where on the disk its first
  117. cluster is located.  For simplicity, these are not shown in Figure 6-1, and
  118. indeed, they are in fact stored before any files on a disk.
  119.      When a 360K floppy disk is formatted, DOS sets aside room for 112
  120. directory entries.  Each entry is 32 bytes long, and holds the name of each
  121. file on the disk, its current size, the date and time it was last written
  122. to, its attribute (hidden, read-only, and so forth), and starting cluster
  123. number.  When you open a file, DOS searches each directory entry for the
  124. file name you specified, and once found, goes to the first cluster that
  125. holds the file's data.
  126.      The disk's FAT contains one entry for every cluster in the data area,
  127. to show which clusters are in use and by which file.  The FAT is organized
  128. as a linked list, with each entry pointing to the next.  The last cluster
  129. in the file is identified with a special value.  The FAT also holds other
  130. special values to identify unused, reserved, and defective clusters.
  131.      Because there are a fixed number of directory entries on a disk, it is
  132. possible to receive a "Disk full" message when attempting to open a new
  133. file, even when there is sufficient data space.  The root directory of a
  134. 360K floppy disk is limited to 112 entries, and a 1.2MB disk can hold up to
  135. 224 file names.  Notice that a volume label takes one directory entry,
  136. although no data space is allocated to it.  Unlike the root directory on a
  137. disk, subdirectories that you create are not limited to an arbitrary number
  138. of file name entries.  Rather, a subdirectory *is* in fact a file, and it
  139. can be extended indefinitely until there is no more room on the disk.
  140.      Fortunately, most programmers do not have to deal with disk access at
  141. this level.  When you ask BASIC to open a file and then read from or write
  142. to it, DOS handles all the low-level details for you.  However, I think it
  143. is important to have at least a rudimentary understanding of how disks are
  144. organized.  If you are interested in learning more about the structure of
  145. disks and data files, I recommend Peter Norton's *Programmer's Guide to the
  146. IBM PC & PS/2*.  This excellent reference is published by Microsoft Press,
  147. and can be found at most major book stores.
  148.  
  149.  
  150. DISK-LIKE DEVICES
  151. =================
  152.  
  153. A device is related to a file in that you can open it using BASIC's OPEN
  154. command, and then access it with GET # and PRINT # and the other file-
  155. related BASIC statements.  There are a number of devices commonly used with
  156. personal computers, and these include printers, modems, tape backup units,
  157. and the console (the PC's keyboard and display screen).  Some of these
  158. devices are maintained by DOS, and others are also controlled by BASIC.
  159.      For example, when you open "SCRN:" for Output mode in a BASIC program,
  160. BASIC takes responsibility for displaying the characters that you print. 
  161. However, if you instead open "CON", BASIC merely sends the data to DOS,
  162. which in turn sends it to the display screen.  Any device whose name is
  163. followed by a colon is considered a to be BASIC device; the absence of a
  164. trailing colon indicates a DOS device.  This is important to understand,
  165. because there may be situations when you want to route your program's
  166. output directly through DOS, and not have it be intercepted by BASIC.
  167.      One such situation would be when printing the special control
  168. characters that the ANSI.SYS device driver recognizes.  Normally, BASIC
  169. processes data in a PRINT statement by writing directly to screen memory. 
  170. This provides the fastest response, which is of course desirable in most
  171. programs.  But ANSI.SYS operates by intercepting the stream of characters
  172. sent through DOS.  Since BASIC normally bypasses DOS for screen operations,
  173. ANSI.SYS never gets a chance to see those characters.
  174.      Another reason for printing through DOS is to activate TSR (Terminate
  175. and Stay Resident) programs that intercept the BIOS video routines.  (When
  176. data is sent through DOS for display, DOS merely passes it on to the BIOS
  177. routines which do the real work.)  For example, some early screen design
  178. utilities use this method, to accommodate multiple programming languages by
  179. avoiding the differences in calling and linking.  Therefore, to activate,
  180. say, a pop-up help screen, you are required to print a special control
  181. string.  One such utility uses two CHR$(255) bytes followed by the name of
  182. the screen to be displayed.
  183.      Although this method is very clumsy when compared to newer products
  184. that provide BASIC-linkable object files, it is simpler for the vendor than
  185. providing different objects for each supported language.  This also allows
  186. screens to be displayed from within a batch file using the ECHO command. 
  187. Therefore, if you need to send data through DOS or the BIOS for whatever
  188. reason, you would open and print to the "CON" device, instead of using
  189. normal PRINT statements or printing to the "SCRN:" device.
  190.      One final point worth mentioning is the value of using the same syntax
  191. for both files and devices.  Many programs let the user specify where a
  192. report is to be sent--either to a disk file, a printer, or the screen. 
  193. Rather than duplicate similar code three times in a program, you can simply
  194. assign a string variable to the appropriate device or file name.  This is
  195. shown in the listing below.
  196.  
  197.  
  198. PRINT "Printer, Screen, or File? (P/S/F): ";
  199.  
  200. DO
  201.   Choice$ = UCASE$(INKEY$)
  202. LOOP UNTIL INSTR(" PSF", Choice$) > 1
  203.  
  204. IF Choice$ = "P" THEN
  205.   Report$ = "LPT1:"
  206. ELSEIF Choice$ = "S" THEN
  207.   Report$ = "SCRN:"
  208. ELSE
  209.   PRINT
  210.   LINE INPUT "Enter a file name: ", Report$
  211. END IF
  212.  
  213. OPEN Report$ FOR OUTPUT AS #1
  214.   PRINT #1, Header$
  215.   PRINT #1, SomeStuff$
  216.   PRINT #1, MoreStuff$
  217.   ...
  218.   ...
  219. CLOSE #1
  220. END
  221.  
  222.  
  223. Here, the same block of code can be used regardless of where the report is
  224. to be sent.  The only alternative is to duplicate similar code three times
  225. using PRINT statements if the screen was specified, LPRINT if they want the
  226. printer, or PRINT # if the report is being sent to a file.  Of course, this
  227. example could be further expanded to prompt for a printer number (1, 2, or
  228. 3) if a printer is specified.
  229.  
  230.  
  231. EXPLORING DATA FILES
  232. ====================
  233.  
  234. All data is stored on disk as a continuous stream of binary information,
  235. regardless of how the file was opened.  Even though BASIC and other
  236. languages offer a number of different file access methods, all disk files
  237. merely contain a series of individual bytes.  When you open a file for
  238. random access, you are telling BASIC that it is to treat those bytes in a
  239. particular manner.  In this case, the file is comprised of one or more
  240. fixed-length records.  Thus, BASIC can perform many of the low level
  241. details that help you to organize and maintain that data.
  242.      Likewise, opening a file for INPUT tells BASIC that you plan to read
  243. variable-length string data.  Rather than reading or writing a single block
  244. of a given length, BASIC instead knows to continue to read bytes from the
  245. file until a terminating comma or carriage return is encountered.  However,
  246. in both of these cases the disk file is still comprised of a series of
  247. bytes, and the access method you specify merely tells BASIC how it is to
  248. treat those bytes.
  249.      The short program below illustrates this in context, and you can
  250. verify that all three files are identical using the DOS COMP utility
  251. program.
  252.  
  253.  
  254. OPEN "File1" FOR OUTPUT AS #1
  255.   PRINT #1, "Testing"; SPC(13);
  256. CLOSE
  257.  
  258. OPEN "File2" FOR BINARY AS #1
  259.   Work$ = "Testing" + SPACE$(13)
  260.   PUT #1, , Work$
  261. CLOSE
  262.  
  263. OPEN "File3" FOR RANDOM AS #1 LEN = 20
  264.   FIELD #1, 20 AS Temp$
  265.   LSET Temp$ = "Testing"
  266.   PUT #1
  267. CLOSE
  268. END
  269.  
  270.  
  271. In fact, even executable program files are indistinguishable from data
  272. files, other than by their file name extension.  Again, it is how you
  273. choose to view the file contents that determines the actual form of the
  274. data.
  275.  
  276.  
  277. FILE BUFFERS
  278.  
  279. Before I explain the various file access methods that BASIC provides, there
  280. is one additional low-level detail that needs to be addressed: file
  281. buffers.  A file buffer is a portion of memory that holds data on its way
  282. to and from a disk file, and it is used to speed up file reads and writes.
  283.      As you undoubtedly know, accessing a disk drive is one of the slowest
  284. operations that occurs on a PC.  Because disk drives are mechanical, data
  285. being read or written requires a motor that spins the actual disk, as well
  286. as a mechanism to move the drive head to the appropriate location on the
  287. disk surface.  Even if a file is located in contiguous disk clusters, a
  288. substantial amount of mechanical activity is required during the course of
  289. accessing a large file.
  290.      When you open a file for reading, DOS uses a section of memory that it
  291. allocated on bootup as a disk buffer.  The first time the file is accessed,
  292. DOS reads an entire sector into memory, even if your program requests only
  293. a few bytes.  This way, when your program makes a subsequent read request,
  294. DOS can retrieve that data from memory instead of from the disk.  This
  295. provides an enormous performance boost, since memory can be accessed many
  296. times faster than any mechanical disk drive.  Even if the next portion of
  297. data being read is located in the same sector, the disk drive must wait for
  298. the disk to spin until that sector arrives at the magnetic read/write head. 
  299.      When using a floppy disk the time delays are even worse.  Once a
  300. second or two have passed after accessing a floppy disk, the motor is
  301. turned off automatically.  Having to then restart it again imposes yet
  302. another one or two second delay.
  303.      Similarly, when you write data to a file DOS simply stores the data in
  304. the buffer, instead of writing it to the disk.  When the buffer becomes
  305. full (or when you close the file--whichever comes first), DOS writes the
  306. entire buffer contents to the disk all at once.  Again, this is many times
  307. faster than accessing the physical drive every time data is written.
  308.      You can control the amount of memory that DOS sets aside for its
  309. buffers with a BUFFERS= statement in the PC's CONFIG.SYS file.  For each
  310. buffer you specify, 512 bytes of memory is taken and made unavailable for
  311. other uses.  Even though you might think that more buffers will always be
  312. faster than fewer, this is not necessarily the case.  For each buffer, DOS
  313. also maintains a table that shows which disk sectors the buffer currently
  314. holds.  At some point it can actually take longer for DOS to search through
  315. this table than to read the sector from disk.  Of course, this time depends
  316. on the type of disk (floppy or hard), and the disk's access speed.
  317.      Although DOS' use of disk buffers greatly improves file access speed,
  318. there is still room for improvement.  Each call to DOS to read or write a
  319. file takes a finite amount of time, because most DOS services are handled
  320. by the same interrupt service routine.  Which particular service a program
  321. wants is specified in one of the processor's registers, and determining
  322. which of the many possible services has been requested takes time.
  323.      To further improve disk access performance, BASIC performs additional
  324. file buffering using its own routines.  Since BASIC's buffers are usually
  325. located in near memory, they can also be accessed very quickly, because
  326. additional steps are needed to access data outside of DGROUP.  However,
  327. BASIC PDS [and VB/DOS] store file buffers in the same segment used for
  328. string variables, so there is slightly less improvement when far strings
  329. are being used.  When you open a random access file, a block of memory
  330. large enough to hold one entire record is set aside in string memory.  If a
  331. record length is given as part of the OPEN command with LEN =, BASIC uses
  332. that for the buffer size.  Otherwise, it uses the default size of 128
  333. bytes.
  334.      When you open a file for sequential access, BASIC also allocates
  335. string memory for a buffer.  512 bytes are used by default, though you can
  336. override that with the optional LEN = argument.  Specifying a buffer size
  337. with non-random files will be discussed later in this chapter.
  338.      Note that BASIC PDS does not create a buffer when a file is opened for
  339. random access and you are using far strings.  If a subsequent FIELD
  340. statement is then used, the fielded strings themselves comprise the buffer. 
  341. Otherwise, BASIC assumes you will be reading the data into a TYPE variable,
  342. and avoids the extra buffering altogether.  Also, file buffers in a BASIC
  343. PDS program are always stored in string memory, which is not necessarily
  344. DGROUP.  If you are in the QBX environment or have compiled with the /fs
  345. far strings option, all file buffers will be stored in the far string data
  346. segment.
  347.      Although BASIC's additional file buffering does improve your program's
  348. speed, it also comes at a cost: the buffers take away from string memory,
  349. and the only way to release their memory is to flush their contents to disk
  350. by closing the file.  DOS offers a service to purge a file's buffers, to
  351. ensure that the data will be intact even if the program is terminated
  352. abnormally or the power is turned off.  Therefore, it is considered good
  353. practice to periodically close a file during long data entry sessions.  But
  354. closing the file and then reopening it after writing each record takes a
  355. long time, and more than negates any advantage offered by BASIC's added
  356. buffering.  [Also, the DOS service that flushes a file's buffers does *not*
  357. flush BASIC's buffers.  Any data you have written to disk that is still
  358. pending in a BASIC buffer will not be written to the file by this service.]
  359.      It is interesting to note that BASIC always closes all open files when
  360. a program ends, so it is not strictly necessary to do that manually.  I
  361. mention this only because you can save a few bytes by eliminating the CLOSE
  362. command.  Also, DOS flushes its buffers and closes all open files when a
  363. program ends, so a few bytes can be saved this way even with non-BASIC
  364. programs.  Again, I am not necessarily recommending that you do this, and
  365. some programmers would no doubt disagree with such advice.  But the fact is
  366. that an explicit CLOSE is not truly needed.
  367.  
  368.  
  369. FILE ACCESS METHODS
  370. ===================
  371.  
  372. BASIC offers three fundamental methods for accessing files, and these are
  373. specified when the file is opened.  There are also several variations and
  374. options available with each method, and these will be discussed in more
  375. detail in the sections that describe each method.
  376.      The first access method is called Sequential, because it requires you
  377. to read from or write to the file in a continuous stream.  That is, to read
  378. the last item in a sequential file you must read all of the items that
  379. precede it.  There are three different forms of OPEN for accessing
  380. sequential files.
  381.      OPEN FOR OUTPUT creates the named file if it does not yet exist, or
  382. truncates it to a length of zero if it does.  Once a file has been opened
  383. for output, you may only write data to it.
  384.      OPEN FOR APPEND is related to OPEN FOR OUTPUT, and it also tells BASIC
  385. to open the file for writing.  Unlike OPEN FOR OUTPUT, however, OPEN FOR
  386. APPEND does not truncate a file if it already exists.  Rather, it opens the
  387. file and then seeks to the place just past the last byte.  This way, data
  388. that is subsequently written will be appended to the end of the file.  Note
  389. that OPEN FOR APPEND will also create a file if it does not already exist.
  390.      OPEN FOR INPUT requires that the named file be present; otherwise, a
  391. "File not found" error will result.  Once a file has been opened for input,
  392. you may only read from it.
  393.      BASIC also offers the SEEK command to skip to any arbitrary position
  394. in the file, and SEEK can in fact be used with sequential files.  However,
  395. sequential files are generally written using a comma or a carriage
  396. return/line feed pair, to indicate the end of each data item.  Since each
  397. item can be of a varying length, it is difficult if not impossible to
  398. determine where in the file a given item begins.  That is, if you wanted to
  399. read, say, the 200th line in a README file, how could you know where to
  400. seek to?
  401.      The second primary file access method is Random, and it allows you to
  402. read from and write to the file.  When you use OPEN FOR RANDOM, BASIC knows
  403. that you will be accessing fixed-length blocks of data called *records*. 
  404. The advantage of random access is that any record can be accessed by a
  405. record number, instead of having to read through the entire file to get to
  406. a particular location.  That is, you can read or write any record randomly,
  407. without regard to where it is in the file.  Because each record has the
  408. same physical length as every other record, it is easy for BASIC to
  409. calculate the location in the file to seek to, based on the desired record
  410. number and the fixed record length.
  411.      Using random access is ideal for data that is already organized as
  412. fixed-length records such as you would find in a name and address database. 
  413. Since each record contains the same amount of information, there is a
  414. natural one-to-one correspondence between the data and the record number in
  415. which it resides.  For example, the data for customer number 1 would be
  416. stored in record number 1, customer 2 is stored in record 2, and so forth.
  417.      Random access can also be used for text and other document files;
  418. however, that is much less common.  Although this would let you quickly
  419. access any arbitrary line of text in the file, the tradeoff is a
  420. considerable waste of disk resources.  For each line, space equal to the
  421. longest one must be set aside for all of them.  In a typical document file
  422. line lengths will vary greatly, and it is wasteful to set aside, say, 80
  423. bytes for each line.
  424.      The third access method is Binary, which is a hybrid of sequential and
  425. random access.  A binary file is opened using the OPEN FOR BINARY command,
  426. and like random, BASIC lets you both read and write the file.  Binary
  427. access is most commonly used when the data in the file is neither fixed-
  428. length in nature, nor delimited by commas or carriage returns.  One example
  429. of a binary file is a Lotus 1-2-3 worksheet file.  Each cell's contents
  430. follows a well-defined format, but varying types of information are
  431. interspersed throughout the file.
  432.      For example, an 8-byte double-precision number may be followed by a
  433. variable length text field, which is in turn followed by the current column
  434. width represented as a 2-byte integer.  Another example of binary
  435. information is the header portion of a dBASE data file.  Although the data
  436. itself is of a fixed length, a block of data is stored at the beginning of
  437. every dBASE data file to indicate the number of fields in each file and
  438. their type.  [Naturally, the length of this header will vary depending on
  439. the number of fields in each record.]  An example program to read Lotus
  440. worksheet files is given later in this chapter, and a program to read and
  441. process dBASE files is shown in Chapter 7.
  442.      Note that BASIC imposes its own rules on what you may and may not do
  443. with each file access method.  This is unfortunate, because DOS itself has
  444. no such restrictions.  That is, DOS allows you to open a file for output,
  445. and then freely read from the same file.  To do this with BASIC you must
  446. first close the file, and then open it again for input.  You can bypass
  447. BASIC entirely if you want, to open files and then read and write them. 
  448. This requires using CALL Interrupt, and examples of doing this will be
  449. shown in Chapter 12.
  450.      BASIC offers two different forms of the OPEN command.  The more common
  451. method--and the one I prefer--is as follows:
  452.  
  453.      OPEN FileName$ FOR OUTPUT AS #FileNum [LEN = Length].
  454.  
  455. Of course, OUTPUT could be replaced with RANDOM, BINARY, INPUT, or APPEND. 
  456. The other syntax is more cryptic, and it uses a string to specify the file
  457. mode.  To open a file for output using the second method you'd use this:
  458.  
  459.      OPEN "O", #FileNum, FileName$, [Length]
  460.  
  461. The first syntax is available only in QuickBASIC and the other current
  462. versions of the BASIC compiler.  The second is a holdover from GW-BASIC,
  463. and according to Microsoft is maintained solely for compatibility with old
  464. programs.  The available single-letter mode designators are "O" for output,
  465. "I" for input, "R" for random, "A" for append, and "B" for binary.  Note
  466. that "B" is not supported in GW-BASIC, and was added beginning with
  467. QuickBASIC version 4.0.
  468.      Besides being more obscure and harder to read, the older syntax does
  469. not let you specify the various access and sharing options available in the
  470. newer syntax.  One advantage of the older method is that you can defer the
  471. open mode until the program runs.  That is, a string variable can be used
  472. to determine how the file will be opened.  However, there are few
  473. situations I can envision where that would be useful.  Of course, the
  474. choice is yours, and some programmers continue to use the original version.
  475.  
  476.  
  477. FILE MANIPULATION STATEMENTS
  478. ============================
  479.  
  480. BASIC offers a number of different statements for opening and manipulating
  481. files.  In a few cases, the same command may have different meanings,
  482. depending on how the file is opened.  For example LEN = mentioned earlier
  483. assumes a different default value when a file is opened for random access
  484. compared to when it is opened for output.  Similarly, GET # may or may not
  485. accept or require a variable name and optional seek offset, depending on
  486. the file mode.  Therefore, pay close attention to each statement as it is
  487. described in the sections that follow.  Specific differences will be listed
  488. as they relate to each of the various file access methods.
  489.  
  490.  
  491. OPENING AND CLOSING FILES
  492.  
  493. Before any file or device may be accessed, it must first be opened with
  494. BASIC's OPEN statement.  When you use OPEN, it is up to you make up a file
  495. number that will be used when you reference the file later.  If you use
  496. OPEN "MYDATA" FOR OUTPUT AS #1, then you will also use the same file number
  497. (1) when you subsequently print to the file.  For example, you might use
  498. PRINT #1, Any$.  Initially, it might appear that letting the programmer
  499. determine his or her own file numbers is a feature.  After all, you are
  500. allowed to make up your own variable names, so why not file numbers too? 
  501. Indeed, BASIC is rare among the popular languages in this regard; both C
  502. and Pascal require that the programmer remember a file number that is given
  503. to them.
  504.      There are several problems with BASIC's use of file numbers, and in
  505. fact DOS does not use this method either.  Instead, DOS returns a *file
  506. handle* when a file has been successfully opened.  When an assembly
  507. language program (or BASIC itself) calls DOS to open a file, it is DOS who
  508. issues the number, and not the program.  BASIC must therefore maintain a
  509. translation table to relate the numbers you give to the actual handles that
  510. DOS returns.  This table requires memory, and that memory is taken from
  511. DGROUP.
  512.      But there is another, more severe problem with BASIC's use of file
  513. numbers instead of DOS handles, because it is possible that you could
  514. accidentally try to open more than one file using the same number.  In a
  515. small program that opens only one or two files, it is not difficult to
  516. remember which file number goes with which file.  But when designing
  517. reusable subroutines that will be added to more than one program, it is
  518. impossible to know ahead of time what file numbers will be in use.
  519.      To solve this problem, Microsoft introduced the FREEFILE function with
  520. QuickBASIC 4.0.  FREEFILE was described in Chapter 4, but it certainly
  521. bears a brief mention again here.  Each time you use FREEFILE it returns
  522. the next available file number, based on which numbers are already taken. 
  523. Therefore, any subroutine that needs to open a file can use the number
  524. FREEFILE returns, confident that the number is not already in use.
  525.      Unless you specify otherwise, a file that has been opened for RANDOM
  526. or BINARY can be both read from and written to.  The ACCESS option of the
  527. OPEN statement lets you indicate that a random or binary file may be read
  528. or written only.  Even though you may ask for both READ and WRITE access
  529. when the file is opened, read/write permission is the default.  In some
  530. cases you may need to open a file for binary access, and also prevent your
  531. program from later writing to it.  In that case you would use the ACCESS
  532. READ option.
  533.      Likewise, specifying ACCESS WRITE tells BASIC to let your program
  534. write to the file, but prevent it from reading.  This may seem nonsensical,
  535. but one situation in which write-only access might be desirable is when
  536. designing a network mail system.  In that case it is quite likely that a
  537. program would be permitted to send mail to another user's electronic
  538. "mailbox", but not be allowed to read the mail contained in that file.  The
  539. various ACCESS options are intended for use with any version of DOS higher
  540. than 2.0.
  541.      Frankly, these ACCESS options are pointless, because if you wrote the
  542. program then you can control whether the file is read from or written to. 
  543. If you are writing the Send Mail portion of a network application, then you
  544. would disallow reading someone else's mail as part of the program logic. 
  545. And if you do open a file for ACCESS WRITE, BASIC will generate an error if
  546. you later try to read from it.  So I personally don't see any real value in
  547. using these ACCESS arguments.
  548.      The remaining two OPEN options are LOCK and SHARED, and these are
  549. meant for use with shared files under DOS 3.0 or later.  Shared access is
  550. primarily employed on a network, though it is possible to share files on a
  551. single computer.  This could be the case when a file needs to be accessed
  552. by more than one program when running under a task-switching program such
  553. as Microsoft Windows.
  554.      You can specify that a file is to be shared by simply adding the
  555. SHARED clause to the OPEN statement.  Thus, another program could both read
  556. and write the file, even while it is open in your program.  To specify
  557. shared access but prevent other programs from writing to the file you would
  558. use LOCK WRITE.  Similarly, using LOCK READ lets another program write to
  559. the file but not read from it, and LOCK READ WRITE prevents both.
  560.      The LOCK statement can optionally be used on a shared file that is
  561. already open to prohibit another program from accessing it only at certain
  562. times.  The LOCK statement allows all or just a portion of a file to be
  563. locked, and the UNLOCK statement releases the locks that were applied
  564. earlier.  Please understand that these network operations are described
  565. here just as a way to introduce what is possible.  Network and database
  566. programming will be described in depth in Chapter 7.
  567.      Finally, you close an open file using BASIC's CLOSE command.  CLOSE
  568. accepts one or more file numbers separated by commas, or no numbers at all
  569. which means that every open file is to be closed.  You can also use the
  570. RESET command to close all currently open files.  When a file that has been
  571. opened for one of the output modes is closed, its file buffer is flushed to
  572. disk and DOS updates the directory entry for that file to indicate the
  573. current date and time and new file size.  Closing any type of file releases
  574. the buffer memory back to BASIC's string memory pool for other uses.
  575.  
  576.  
  577. READING AND WRITING DATA
  578.  
  579. Once a file has been opened you can read from it, write to it, or both,
  580. depending on what form of OPEN was used.  Any file that has been opened for
  581. input may be read from only.  Unlike the BASIC-related limitations I
  582. mentioned earlier, DOS imposes this restriction, and for obvious reasons. 
  583. However, when you open a file for output or append, it is BASIC that
  584. prevents you from reading back what you wrote.  BASIC imposes several other
  585. unfortunate limitations regarding what you can and cannot do with an open
  586. file, as you will see momentarily.
  587.      Sequential access is commonly used with devices as well as with files. 
  588. Although it is possible to open a printer for random access, there is
  589. little point since data is always printed sequentially.  Similarly, reading
  590. from the keyboard or writing to the screen must be sequential.  In the
  591. discussions that follow, you can assume that what is said about accessing
  592. files also applies to devices, unless otherwise noted.
  593.  
  594.  
  595. Sequential Output
  596.  
  597. Data is written to a sequential file using the PRINT # statement, using the
  598. same syntax as the normal PRINT statement when printing to the display
  599. screen.  That is, PRINT # accepts an optional semicolon to suppress a
  600. carriage return and line feed from being written to the file, or a comma to
  601. indicate that one or more blank spaces is to be written after the data. 
  602. The number of blanks sent to the file depends on the current print
  603. position, just like when printing to the screen.
  604.      You can also use the WRITE # statement to print data to a sequential
  605. file, but I recommend against using WRITE in most situations.  Unlike PRINT
  606. that merely sends the data you give it, WRITE adds surrounding quotes to
  607. all string data, which takes time and also additional disk space.  Since a
  608. subsequent INPUT from the file will just have to remove those quotes which
  609. takes even more time, what's the point?  Further, WRITE does not let you
  610. specify a trailing semicolon or comma.  Although a comma may be used as a
  611. delimiter between items written to disk, the comma is stored in the file
  612. literally when WRITE is used.
  613.      The only time I can see WRITE being useful is for printing data that
  614. will be read by a non-BASIC application that explicitly requires this
  615. format.  Many database and spreadsheet programs let you import comma-
  616. delimited data with quoted strings such as WRITE uses.  These programs
  617. treat each complete line ending with a carriage return as an entire record,
  618. and each comma-delimited item within the line as a field in that record. 
  619. But you should avoid WRITE unless your program really needs to communicate
  620. with other such applications, because it results in larger data files and
  621. slower performance.
  622.      Another use for WRITE is to protect strings that contain commas from
  623. being read incorrectly by a subsequent INPUT statement.  INPUT uses commas
  624. to delimit individual strings, and the quotes allow you to input an entire
  625. string with a single INPUT command.  But BASIC's LINE INPUT does this
  626. anyway, since it reads an entire line of text up to a terminating carriage
  627. return.  You could also add the quotes manually when needed:
  628.  
  629.  
  630. IF INSTR(Work$, ",") THEN
  631.   PRINT #1, CHR$(34); Work$; CHR$(34)
  632. ELSE
  633.   PRINT #1, Work$
  634. END IF
  635.  
  636.  
  637. You may also use TAB and SPC to format the output you print to a file or
  638. device.  For the most part, TAB and SPC operate like their non-file
  639. counterparts, including the need to add an extra empty PRINT to force a
  640. carriage return at the end of a line.  That is, when you use
  641.  
  642.      PRINT Any$; TAB(20)
  643. or
  644.      PRINT #1, SomeVar; SPC(13)
  645.  
  646. BASIC adds a trailing semicolon whether you want it or not.  To force a new
  647. line at that point in the printing process requires an additional PRINT or
  648. PRINT # statement.  This isn't really as much of a nuisance as yet another
  649. code bloater, since an empty PRINT adds 9 bytes of compiler-generated code
  650. and an empty PRINT # adds 18 bytes.
  651.      One important difference between the screen and file versions of TAB
  652. and SPC is the way long strings are handled.  If you use TAB or SPC in a
  653. PRINT statement that is then followed by a string too long to fit on the
  654. current line, the screen version will advance to the next row, and print
  655. the string at the left edge.  This is probably not what you expected or
  656. wanted.  When printing to a file, however, the string is simply written
  657. without regard to the current column.  Column 80 is the default width for
  658. the screen and printer when they have been opened as devices, though you
  659. may change that using WIDTH.
  660.      The WIDTH statement lets you specify at which column BASIC is to
  661. automatically add a carriage return/line feed pair.  The default for a
  662. printer is at column 80.  In most programming situations this behavior is a
  663. nuisance, since many printers can accommodate 132 columns.  After all, why
  664. shouldn't you be allowed to print what you want when you want, without
  665. BASIC intervening to add unexpected and often unwanted extra characters? 
  666. Most programmers disable this automatic line wrapping by using WIDTH #
  667. FileNum, 255 if the printer was opened as a device, or WIDTH LPRINT, 255 if
  668. using LRPINT statements.
  669.      Curiously, this special value is not mentioned anywhere in the
  670. otherwise very complete documentation that comes with BASIC PDS.  In fact,
  671. using a width value of 255 is mandatory if you intend to send binary data
  672. to a printer.  Most modern printers accept both graphics commands and
  673. downloadable fonts.  Since either of these will no doubt result in strings
  674. longer than 80 or even 255 characters, it is essential that you have a way
  675. to disable the "favor" that BASIC does for you.  Undoubtedly, the automatic
  676. addition of a carriage return and line feed goes back to the early days of
  677. primitive printers that required this.  The only reason Microsoft continues
  678. this behavior is to assure compatibility with programs written using
  679. earlier versions of BASIC.
  680.      Related to the WIDTH anomaly is BASIC's insistence on adding a
  681. CHR$(10) line feed whenever you print a CHR$(13) carriage return to a
  682. device.  Again, this dubious feature is provided on the assumption that you
  683. would always want a line feed after every carriage return.  But there are
  684. many cases where you wouldn't, such as the font and graphics examples
  685. mentioned earlier.  If you add the "BIN" (binary) option when opening a
  686. printer, you can prevent BASIC from forcing a new line every 80 columns,
  687. and also suppress the addition of a line feed following each carriage
  688. return.  For example, OPEN "LPT1:BIN" FOR OUTPUT AS #1 tells BASIC to open
  689. the first parallel printer in binary mode.
  690.      The PRINT # USING statement lets you send formatted numeric data to a
  691. file, in the same way you would use the regular PRINT USING to format
  692. numbers on the screen.  PRINT # USING accepts the same set of formatting
  693. commands as PRINT USING, allowing you to mix text and formatted numbers in
  694. a single PRINT operation.  If your program will be printing formatted
  695. reports from the disk file later, I recommend using PRINT USING at that
  696. time, instead of when writing the data to disk.  Otherwise, the extra
  697. spaces and other formatting information are added to the file increasing
  698. its size.  In fact, PRINT # USING is really most appropriate when printing
  699. to a device such as a printer.
  700.      Finally, it is important to point out the importance of selecting a
  701. suitable buffer size.  As I described earlier, BASIC and DOS employ an area
  702. of memory as a buffer to hold information on its way to and from disk. 
  703. This way information can often be written to or read from memory, instead
  704. of having to access the physical disk each time.  Besides the buffers that
  705. DOS maintains, BASIC provides additional buffering when your program is
  706. using sequential input or output.
  707.      BASIC lets you control the size of this buffer, using the LEN = option
  708. of the OPEN statement.  In general, the larger you make the buffer, the
  709. faster your programs will read and write files.  The trade-off, however, is
  710. that BASIC's buffers are stored in string memory.  With QuickBASIC and near
  711. strings in BASIC PDS, the buffer is located in DGROUP.  When BASIC PDS far
  712. strings are used, the buffer is in the same segment that the current module
  713. uses for string storage.
  714.      Conversely, you can actually reduce the default buffer size when
  715. string space is at a premium, but at the expense of disk access speed. 
  716. When using OPEN FOR INPUT and OPEN FOR OUTPUT, BASIC sets aside 512 bytes
  717. of string memory for the buffer, unless you specify otherwise.  If you have
  718. many sequential files open at once you could reduce the buffer sizes to 128
  719. bytes, for a net savings of 384 bytes for each file.  The legal range of
  720. values for LEN = is between 1 and 32767 bytes.
  721.      Notice that the best buffer values will be a multiple of a power of
  722. two, and when increasing the buffer size, a multiple of 512.  Since a disk
  723. sector is almost always 512 bytes, DOS will fill the buffer with an entire
  724. sector.  In fact, DOS always reads and writes entire sectors anyway.  If
  725. you use a buffer size of, say, 600 bytes, DOS will have to read 1024 bytes
  726. just to get the first portion of the second sector.  But when more data is
  727. needed later, BASIC will then have to go back and ask DOS for the same
  728. information again.  By reading entire sectors or evenly divisible portions
  729. of a sector, you can avoid having BASIC and DOS read the same information
  730. more than once.
  731.      Even though larger buffers usually translate to better performance,
  732. you will eventually reach the point of diminishing returns, beyond which
  733. little performance improvement will result.  Table 6-1 shows the timing
  734. results with various buffer sizes when reading a 104K BASIC source file
  735. using LINE INPUT.  Understand that this test is informal, and merely shows
  736. the results obtained using only one PC.  In particular, the hard disk
  737. results are for a fairly fast (17 millisecond) 150 MB ESDI drive and a PC
  738. equipped with a 25 MHz. 386.  Therefore, the improvement from a larger
  739. buffer is less than you would get on a slower computer with a slower hard
  740. disk or with a floppy disk.  Many older XT and AT compatible PCs will
  741. probably fall somewhere between the results shown here for the hard and
  742. floppy disks.  Notice that while the improvement actually seems somewhat
  743. worse for some increases, this can be attributed to the lack of resolution
  744. in the PC's system timer.
  745.  
  746. Fast ESDI hard disk:
  747.  
  748. Buffer Size (in bytes)         Seconds
  749. ----------------------         -------
  750.           64                    2.699
  751.          128                    2.420
  752.          256                    2.410
  753.          512                    2.420
  754.         1024                    2.311
  755.         2048                    2.139
  756.         4096                    2.201
  757.         8192                    2.080
  758.        16384                    2.039
  759.  
  760.  
  761. 360K floppy disk: 
  762.  
  763. Buffer Size (in bytes)         Seconds
  764. ----------------------         -------
  765.           64                   45.260
  766.          128                   45.141
  767.          256                   45.148
  768.          512                   45.150
  769.         1024                   27.180
  770.         2048                   18.180
  771.         4096                   13.570
  772.         8192                   11.650
  773.        16384                   11.371
  774.  
  775. Table 6-1: Timing Results For Sequential Reading Versus Buffer Size.
  776.  
  777. It is important to point out that a buffer is created only for sequential
  778. input and output, and also for random files with QuickBASIC.  Opening a
  779. file for random access with BASIC PDS [and I'll presume VB/DOS] does not
  780. create a buffer, nor does opening a file for binary with either version. 
  781. Further, with random access files a buffer is created by QuickBASIC only
  782. when FIELD is used, and the buffer is located within the actual fielded
  783. strings.  Therefore, the LEN = argument in an OPEN FOR RANDOM statement
  784. merely tells BASIC how to calculate record offsets when SEEK and GET are
  785. used.  
  786.  
  787.  
  788. Sequential Input
  789.  
  790. Sequential data is read using INPUT #, LINE INPUT #, or INPUT$ #.  Like the
  791. console form of INPUT, INPUT # can be used to read one or more variables of
  792. any type and in any order with a single statement.  When reading a file,
  793. INPUT # recognizes both the comma and the carriage return as a valid
  794. delimiter, to indicate the end of one variable.  This is in contrast to the
  795. regular [keyboard] version of INPUT, which issues a "Redo from start" error
  796. if the wrong number of comma-delimited variables are entered.  Instead,
  797. INPUT # simply moves on to the next line for the remaining variables.
  798.      LINE INPUT # avoids this entirely, and simply reads an entire string
  799. without regard to commas until a carriage return is encountered.  This
  800. precludes LINE INPUT # from being used with anything but string variables. 
  801. However, LINE INPUT # can be used with fixed- as well as variable-length
  802. strings, without the overhead of copying from one type to the other that
  803. BASIC usually adds.  [This copying was described in Chapter 2.]  As with
  804. INPUT #, LINE INPUT # strips leading and trailing quotes from the line if
  805. they are present in the file.
  806.      The last method for reading a sequential file or device is with the
  807. INPUT$ # function.  INPUT$ # is used to read a specified number of
  808. characters, without regard to their meaning.  Where commas and carriage
  809. returns are normally used to delimit each line of text, INPUT$ returns them
  810. as part of the string.  INPUT$ # accepts two arguments--the number of
  811. characters to read and the file number--and assigns them to the specified
  812. string.  To read, say, 20 bytes from a sequential file that has been opened
  813. as #3, you would use Any$ = INPUT$(20, #3).  Although the pound sign (#) is
  814. optional, I prefer to include it to avoid confusion as to which parameter
  815. is the file number and which is the number of bytes.
  816.      As with sequential output, specifying a larger buffer size than the
  817. default 512 bytes can greatly improve the speed of INPUT # and LINE INPUT #
  818. statements, but at the expense of string memory.
  819.  
  820.  
  821. Random Access
  822.  
  823. Unlike sequential files that are almost always read starting at the
  824. beginning, data in a random access file can be accessed literally in any
  825. arbitrary order.  Random access files are comprised of fixed-length
  826. *records*, and each record contains one or more *fields*.  The most common
  827. application of random access techniques is in database programs, where each
  828. record holds the same type of information as the next.  For example, a
  829. customer name and address database is comprised of a first name, a last
  830. name, a street address, city, state, and zip code.  Even though different
  831. names and addresses will be stored in different records, the format and
  832. length of the information in each record is identical.
  833.      BASIC provides two different ways to handle random access files: the
  834. FIELD statement and TYPE variables.  Before QuickBASIC version 4.0, the
  835. FIELD method was the only way to define the structure of a random access
  836. data file.  Although Microsoft has publicly stated that FIELD is provided
  837. in current versions of BASIC only for compatibility with older programs, it
  838. has several important properties that cannot be duplicated in any other
  839. way.  FIELD also lets you perform some interesting an non-obvious tricks
  840. that have nothing to do with reading or writing files.  These are described
  841. later in this chapter in the section *Advanced File Techniques*.
  842.      Once a file has been opened for RANDOM you may use the FIELD statement
  843. by specifying one or more string variables to hold each field, along with
  844. their length.  A typical example showing the syntax for the FIELD statement
  845. is as follows:
  846.  
  847.  
  848. OPEN FileName$ FOR RANDOM AS #1 LEN = 97
  849. FIELD #1, 17 AS LastName$, 14 AS FirstName$, 32 AS Address$, 15 AS City$, _
  850.   2 AS State$, 9 AS Zip$, 8 AS BalanceDue$
  851.  
  852.  
  853. Here, the file is opened for random access, and the record length is
  854. established as being 97 characters.  This allows room for each of the
  855. fields in the FIELD statement.  In this case 17 characters are set aside
  856. for the last name, 14 for the first name, 32 for the street address, 15 for
  857. the city, 2 for the state, 9 for the zip code, and 8 for the double
  858. precision balance due value.  I often use a field length of 32 characters
  859. for name and address data, because that's how many can fit comfortably on a
  860. standard 3-1/2 by 15/16 inch mailing label.  (The first and last names
  861. above add up to 32 characters, including a separating blank space.)
  862.      Note that the underscore shown above is used here as line continuation
  863. character, and you'd actually type the entire statement as one long line. 
  864. In fact, in most cases a FIELD statement must be able to fit entirely on a
  865. single line, and there is no direct way to continue the list of variables. 
  866. Although the BC compiler recognizes an underscore to continue a line as
  867. shown here, the BASIC environment does not.  Underscores in a source file
  868. are removed by the BASIC editor when the file is loaded, and the lines are
  869. then combined.
  870.      If a second FIELD statement for the same file number is given on a
  871. separate line, the additional strings specified are placed starting at the
  872. beginning of the same buffer.  While it is possible to coerce a new FIELD
  873. statement to begin farther into the buffer, that requires an additional
  874. dummy string variable:
  875.  
  876.  
  877. FIELD #1, 17 AS LastName$, 14 AS FirstName$
  878. FIELD #1, 31 AS Dummy$, 32 AS Address$, 15 AS City$
  879. FIELD #1, 78 AS Dummy2$, 2 AS State$, 9 AS Zip$
  880.  
  881.  
  882. Here, the dummy strings are used as placeholders to force the Address$ and
  883. State$ variables farther into the buffer, and you would not refer to the
  884. dummy strings in your program.
  885.      Once a field buffer has been defined, special precautions are needed
  886. when assigning and reading the fielded string variables.  As you know,
  887. BASIC often moves strings around in memory when they are assigned. 
  888. However, that would be fatal if those strings are in a field buffer.  A
  889. field buffer is written to disk all at once when you use PUT, and it is
  890. essential that all of the strings therein be contiguous.  If you simply
  891. assign a variable that is part of a field buffer, BASIC may move the string
  892. data to a new location outside of the buffer and your program will fail.
  893.      To avoid this problem you must assign fielded string using either
  894. LSET, RSET, or the statement form of MID$.  These BASIC commands let you
  895. insert characters into a string, so BASIC will not have to claim new string
  896. memory.  This further contributes to FIELD's complexity, and it also adds
  897. slightly to the amount of code needed for each assignment.  For example,
  898. the statement One$ = Two$ generates 13 bytes of compiled code, and the
  899. statement LSET One$ = Two$ creates 17.  Although LSET is generally faster
  900. than a direct assignment, it is important to understand that it also
  901. creates more code.  But the situation gets even worse.
  902.      Because all of the variables in a field buffer must be strings,
  903. additional steps are needed to assign numeric variables such as integer and
  904. double precision.  The CVI and MKS$ family of BASIC functions are needed to
  905. convert numeric data to their equivalent in string form and back.  There
  906. are eight of these functions in QuickBASIC with two each for integer, long
  907. integer, single precision, and double precision variables.  BASIC PDS adds
  908. two more to support the Currency data type.  All of the various conversion
  909. functions have names that start with the letters MK or CV, and a complete
  910. list can be found in your BASIC manual.
  911.      To convert a double precision variable to equivalent data in an 8-byte
  912. string you would use MKD$, and to convert a 2-byte string that holds an
  913. integer to an actual integer value you would use CVI.  MKD$ stands for
  914. "Make Double into a string" and it has a dollar sign to show that it
  915. returns a string.  CVI stands for "Convert to Integer" and the absence of a
  916. dollar sign shows that it returns a numeric value.  Combined with the
  917. requisite LSET, a complete assignment prior to writing a record to disk
  918. with PUT would be something like this: LSET BalanceDue$ = MKD$(BalDue#). 
  919. And if a record has just been read using GET, an integer value in the field
  920. buffer could be retrieved using code such as MyInt% = CVI(IntVar$).
  921.      The need for LSET, RSET, CVI, and MKS$ and so forth has historically
  922. made learning random access file techniques one of the most difficult and
  923. messy aspects of BASIC programming.  Besides having to learn all of the
  924. statements and how they are used, you also need to understand how many
  925. bytes each numeric data type occupies to set aside the correct amount of
  926. space in the field buffer.  Further, a lot of compiled code is created to
  927. convert large amounts of data between numeric and string form.  For these
  928. and other reasons, Microsoft introduced the TYPE variable with its release
  929. of QuickBASIC 4.0.
  930.      The TYPE method allows you to establish a record's structure by
  931. defining a custom variable that contains individual components for each
  932. field in the record.  In general, using TYPE is a much clearer way to
  933. define a record, and it also avoids the added library code to handle the
  934. FIELD, LSET, CVI, and MKS$ statements.  When you use AS INTEGER and AS
  935. DOUBLE and so forth to define each portion of the TYPE, the correct number
  936. of bytes are allocated to store the value in its native fixed-length
  937. format.  This avoids having to convert the data to and from ASCII digits.
  938.      Using the earlier example, here's how you would define and assign the
  939. same record using a TYPE variable:
  940.  
  941.  
  942. TYPE Record
  943.   LastName AS STRING * 17
  944.   FirstName AS STRING * 14
  945.   Address AS STRING * 32
  946.   State AS STRING * 2
  947.   Zip AS STRING 9
  948.   BalanceDue AS DOUBLE
  949. END TYPE
  950. DIM MyRecord AS Record
  951.  
  952. MyRecord.LastName = LastName$
  953. MyRecord.FirstName = FirstName$
  954. MyRecord.Address = Address$
  955. MyRecord.State = State$
  956. MyRecord.Zip = Zip$
  957. MyRecord.BalanceDue = BalanceDue#
  958.  
  959.  
  960. Even though the same names are used for both the TYPE variable members and
  961. the strings they are being assigned from, you may of course use any names
  962. you want.  You could also assign the portions of a TYPE variable from
  963. constants using MyRecord.Zip = "06896" or MyRecord.BalanceDue = 4029.80. 
  964. Further, one entire TYPE variable may be assigned to another in a single
  965. operation using ThisType = ThatType.  Dissimilar TYPE variables may be
  966. assigned using LSET like this: LSET MyType = YourType.
  967.      As you can see, using TYPE variables instead of FIELD yields an
  968. enormous improvement in a program's clarity.  However, there are still some
  969. programming problems that only FIELD can solve.  One limitation of using
  970. TYPE variables is that the file structure must be known when the program is
  971. compiled, and you cannot defer this until runtime.  Therefore, it is
  972. impossible to design a general purpose database program, in which a single
  973. program can manipulate any number of differently structured files.  The
  974. compiler needs to know the length and type of data within a TYPE variable,
  975. in order to access the data it contains.  So while you can use a variable
  976. as the LEN = argument with OPEN, the record structure itself must remain
  977. fixed.
  978.      FIELD avoids that limitation because it accepts a variable number of
  979. arguments, and varying lengths within each field component.  Therefore, by
  980. dimensioning a string array to the number of elements needed for a given
  981. record, the entire process of opening, fielding, reading, and writing can
  982. be handled using variables whose contents and type are determined at
  983. runtime.  Some amount of IF testing will of course be required when the
  984. program runs, but at least it's possible to process a file using variable
  985. information.
  986.      The following complete program first creates a random access file with
  987. five slightly different records using a TYPE variable.  It then reads the
  988. file independently of the TYPE structure using the FIELD method.  Although
  989. the second portion of the program uses DATA statements to define the file's
  990. structure, in practice this information would be read from disk.  In fact,
  991. this is the method used by dBASE and Clipper files, based on the field
  992. information that is stored in a header portion of the data file.
  993.  
  994. '----- create a data file containing five records
  995. DEFINT A-Z
  996.  
  997. TYPE MyType
  998.   FirstName AS STRING * 17
  999.   LastName AS STRING * 14
  1000.   DblValue AS DOUBLE
  1001.   IntValue AS INTEGER
  1002.   MiscStuff AS STRING * 20
  1003.   SngValue AS SINGLE
  1004. END TYPE
  1005. DIM MyVar AS MyType
  1006.  
  1007. OPEN "MYFILE.DAT" FOR RANDOM AS #1 LEN = 65
  1008. MyVar.FirstName = "Jonathan"
  1009. MyVar.LastName = "Smith"
  1010. MyVar.DblValue = 123456.7
  1011. MyVar.IntValue = 10
  1012. MyVar.MiscStuff = "Miscellaneous stuff"
  1013. MyVar.SngValue = 14.29
  1014. FOR X = 1 TO 5
  1015.   PUT #1, , MyVar
  1016.   MyVar.DblValue = MyVar.DblValue * 2
  1017.   MyVar.IntValue = MyVar.IntValue * 2
  1018.   MyVar.SngValue = MyVar.SngValue * 2
  1019. NEXT
  1020. CLOSE #1
  1021.  
  1022.  
  1023. '----- read the data without regard to the TYPE above
  1024. READ FileName$, NumFields
  1025. REDIM Buffer$(1 TO NumFields)   'holds the FIELD strings
  1026. REDIM FieldType(1 TO NumFields) 'the array of data types
  1027.  
  1028. RecLength = 0
  1029. FOR X = 1 TO NumFields
  1030.   READ ThisType
  1031.   FieldType(X) = ThisType
  1032.   RecLength = RecLength + ABS(ThisType)
  1033. NEXT
  1034.  
  1035. OPEN FileName$ FOR RANDOM AS #1 LEN = RecLength
  1036.  
  1037. PadLength = 0
  1038. FOR X = 1 TO NumFields
  1039.   ThisLength = ABS(FieldType(X))
  1040.   FIELD #1, PadLength AS Pad$, ThisLength AS Buffer$(X)
  1041.   PadLength = PadLength + ThisLength
  1042. NEXT
  1043.  
  1044. NumRecs = LOF(1) \ RecLength    'calc number of records
  1045. FOR X = 1 TO NumRecs            'read each in sequence
  1046.   GET #1                        'get the current record
  1047.   CLS
  1048.   FOR Y = 1 TO NumFields        'walk through each field
  1049.     PRINT "Field"; Y; TAB(15);  'display each field
  1050.     SELECT CASE FieldType(Y)    'see what type of data
  1051.       CASE -8                   'double precision
  1052.         PRINT CVD(Buffer$(Y))   'so use CVD
  1053.       CASE -4                   'single precision
  1054.         PRINT CVS(Buffer$(Y))   'as above
  1055.       CASE -2                   'integer
  1056.         PRINT CVI(Buffer$(Y))
  1057.       CASE ELSE                 'string
  1058.         PRINT Buffer$(Y)
  1059.     END SELECT
  1060.   NEXT
  1061.   LOCATE 20, 1
  1062.   PRINT "Press a key to view the next record ";
  1063.   WHILE LEN(INKEY$) = 0: WEND
  1064. NEXT
  1065. CLOSE #1
  1066. END
  1067.  
  1068. DATA MYFILE.DAT, 6
  1069. DATA 17, 14, -8, -2, 20, -4
  1070.  
  1071. There are several issues that need elaboration in this program.  First is
  1072. the use of arrays to hold the fielded string data and also each field's
  1073. type.  When the field buffer is defined with an array, the same variable
  1074. name can be used repeatedly in a loop.  A parallel array that holds the
  1075. field data types permits the program to relate the field data to its
  1076. corresponding type of data.  That is, Buffer$(3) holds the data for field
  1077. 3, and FieldType(3) indicates what type of data it is.
  1078.      Second, the FieldType array uses a simple coding method that combines
  1079. both the data type and its length into a single value.  That is, positive
  1080. values are used to indicate string data, and the value itself is the field
  1081. length.  Negative values reflect the data type as well as the length, using
  1082. a negative version of that data type's length.  Specifically, -8 is used to
  1083. indicate a double precision field type, -4 a single precision type, and -2
  1084. an integer.  If you need to handle long integers or the BASIC PDS Currency
  1085. data type, you'll need to devise a slightly different method.  I chose this
  1086. one because it is simple and effective.
  1087.      The final point worth mentioning when comparing FIELD to TYPE is that
  1088. the field buffer is relinquished back to BASIC's string pool when the file
  1089. is closed.  But when a TYPE variable is dimensioned, the near memory it
  1090. occupies is allocated by the compiler, and is never available for other
  1091. uses.  Although there is a solution, it requires some slight trickery.  The
  1092. statement REDIM TypeVar(1 TO 1) AS TypeName will create a 1-element TYPE
  1093. array in far memory that can then be used as if it were a single TYPE
  1094. variable.  That is, any place you would have used the TYPE variable, simply
  1095. substitute the sole element in the array.
  1096.      Understand that more code is required to access data in a dynamic
  1097. array than in a static variable.  For example, an integer assignment to a
  1098. member of a dynamic TYPE array generates 17 bytes of code, compared to only
  1099. 6 bytes for the same operation on a static TYPE.  But when string space is
  1100. more important than .EXE file size, this trick can make the difference
  1101. between a program that runs and one that doesn't.
  1102.      Regardless of which method you use--TYPE or FIELD--there are several
  1103. additional points to be aware of.  First, the PUT # and GET # statements
  1104. are used to write and read a random access file respectively.  PUT # and
  1105. GET # accept two different forms, depending on whether you are using TYPE
  1106. or FIELD to define the record structure.
  1107.      When FIELD is used, PUT # and GET # may be used with either no
  1108. argument to access the current record, or with an optional record number
  1109. argument.  That is, PUT #1 writes the current field buffer contents to disk
  1110. at the current DOS SEEK position, and GET #1, RecNum reads record number
  1111. RecNum into the buffer for subsequent access by your program.
  1112.      As with sequential files, each time a record is read or written, DOS
  1113. advances its internal seek location to the next successive position in the
  1114. file.  Therefore, to read a group of records in forward order does not
  1115. require a record number, nor does writing them in that order.  In fact,
  1116. slightly more time is required to access a record when a record number is
  1117. given but not needed, because BASIC makes a separate call to perform an
  1118. explicit Seek to that location in the file.
  1119.      When the TYPE method is used to access random access data, the record
  1120. number is also optional, but you must provide the name of a TYPE variable
  1121. or TYPE array element.  In this case, the record number is still used as
  1122. the first argument, and the TYPE variable is the second argument.  If you
  1123. omit the record number you must include an empty comma placeholder.  For
  1124. example, PUT #1, RecNum, TypeVar writes the contents of TypeVar to the file
  1125. at record number RecNum, and GET #1, , TypeArray(X) reads the current
  1126. record into TYPE array element X.
  1127.      It is not essential that the TYPE variable be as long as the record
  1128. length specified when LEN = was used with OPEN, but it generally should be. 
  1129. When a record number is given with PUT # or GET #, BASIC uses the original
  1130. LEN = value to know where to seek to in the file.  If a record number is
  1131. omitted, BASIC will still advance to the next complete record even if the
  1132. TYPE variable being read or written is shorter than the stated record
  1133. length.  In most cases, however, you should use a TYPE whose length
  1134. corresponds to the LEN = argument unless you have a good reason not to.
  1135.      Notice that when LEN = is omitted, BASIC defaults to a record length
  1136. of 128 bytes.  Indeed, forgetting to include the length can lead to some
  1137. interesting surprises.  One clever trick that avoids having to calculate
  1138. the record length manually is to use BASIC's LEN function.  Although
  1139. earlier versions of BASIC allowed LEN only in conjunction with string
  1140. variables, QuickBASIC 4.0 and later versions recognize LEN for any type of
  1141. data.
  1142.      For example, LEN(IntVar%) is always 2, and LEN(AnyDouble#) is always
  1143. equal to 8.  When LEN is used this way the compiler merely substitutes the
  1144. appropriate numeric constant when it builds your program.  Since LEN can
  1145. also be used with TYPE variables and TYPE array elements, you can let BASIC
  1146. do the byte counting for you.  The brief program fragment below shows this
  1147. in context.
  1148.  
  1149.  
  1150. TYPE Something
  1151.   X AS INTEGER
  1152.   Y AS DOUBLE
  1153.   Z AS STRING * 100
  1154. END TYPE
  1155. DIM Anything AS Something
  1156. OPEN MyData$ FOR RANDOM AS #1 LEN = LEN(Anything)
  1157.  
  1158.  
  1159. In particular, this method is useful if you later modify the TYPE
  1160. definition, since the program will be self-accommodating.  Changing Z to
  1161. STRING * 102 will also change the value used as the LEN = argument to OPEN. 
  1162. Be careful to use the actual variable name with LEN, and not the TYPE name
  1163. itself.  That is, LEN(Anything) will equal 110, but LEN(Something) will be
  1164. 2 if DEFINT is in effect.  When BASIC sees LEN(Something) it assumes you
  1165. are referring to a variable with that name, not the TYPE definition.
  1166.      The only time this use of LEN will be detrimental is when it is used
  1167. as a passed parameter many times in a program.  Since LEN is treated in
  1168. this case as a numeric constant, it is subject to the same copying issues
  1169. that CONST values and literal numbers are.  Therefore, you would probably
  1170. want to assign a variable once from the value that LEN returns, and use
  1171. that variable repeatedly later as described in Chapter 2.
  1172.  
  1173.  
  1174. Binary Access
  1175.  
  1176. Binary file access lets you read or write any portion of a file, and
  1177. manipulate any type of information.  Reading a sequential file requires
  1178. that the end of each data item be identified by a comma, or a carriage
  1179. return line feed pair.  Random access files do not require special
  1180. delimiters, and instead rely on a fixed record length to know where each
  1181. record's data starts and ends.  A binary file may be organized in any
  1182. arbitrary manner; however, it is up to the programmer to devise a method
  1183. for determining what goes where in the file.
  1184.      The overwhelming advantage of binary over sequential access is the
  1185. enormous space and speed savings.  A file that requires extra carriage
  1186. returns or commas will be larger than one that does not.  Moreover, numeric
  1187. data in a binary file is stored in its native fixed-length format, instead
  1188. of as a string of ASCII digits.  Therefore, the integer value -32700 will
  1189. occupy only two bytes, as opposed to the seven needed for the digits plus
  1190. either a comma or carriage return and line feed.
  1191.      Furthermore, converting between numbers and their ASCII representation
  1192. is one of the slowest operations in BASIC.  Because the STR$ and VAL
  1193. functions must be able to operate on floating point numbers and perform
  1194. rounding, they are extremely slow.  For example, VAL must examine the
  1195. digits in a string for many special characters such as "e", "d", "&H", and
  1196. so forth.  And with the statement IntVar% = VAL("1234.56"), VAL must also
  1197. round the value to 1235 before assigning the result to IntVar%.  Even if
  1198. you don't use STR$ or VAL explicitly when reading or writing a file, BASIC
  1199. does internally.  That is, the statement PRINT #1, D# is compiled as if you
  1200. used PRINT #1, STR$(D#).  Likewise, INPUT #1, IntVar% is compiled the same
  1201. as INPUT #1, Temp$: IntVar% = VAL(Temp$).
  1202.      When a file has been opened for binary access you may not use PRINT #,
  1203. WRITE #, or PRINT # USING.  The only statement that can write data to a
  1204. binary file is PUT #.  PUT # may be used with any type of variable, but not
  1205. constants or expressions.  That is, you can use PUT #1, , AnyVar, but not
  1206. PUT #1, , 13 or PUT #1, SeekLoc, X + Y! or PUT #1, , LEFT$(Work$, 10). 
  1207. This is yet another unnecessary BASIC limitation, which means that to write
  1208. a constant you must first assign it to a temporary variable, and then use
  1209. PUT specifying that variable.
  1210.      Reading from a binary file requires GET #, which is the complement of
  1211. PUT #.  Like PUT #, GET # may be used with any kind of variable, including
  1212. TYPE variables.  When a string variable is written to disk with PUT #, the
  1213. entire string is sent.  However, when a string variable is used with GET #,
  1214. BASIC reads only as many bytes as will fit into the target string.  So to
  1215. read, say, 20 bytes into a string from a binary file you would use this:
  1216.  
  1217.      Temp$ = SPACE$(20)       'make room for 20 bytes
  1218.      GET #FileNum, , Temp$    'read all 20 bytes
  1219.  
  1220. Although fixed-length strings cannot be cleared to relinquish the memory
  1221. they occupied, they are equally valid for reading data from a binary file:
  1222.  
  1223.      DIM FLen AS STRING * 20
  1224.      GET #FileNum, , FLen
  1225.  
  1226. You can also use INPUT$ to read a specified number of bytes from a binary
  1227. file.  Therefore you can replace both examples above with the statement
  1228. Temp$ = INPUT$(20, #FileNum).  Contrary to some versions of Microsoft BASIC
  1229. documentation, PUT # does not store the length of the string in a binary
  1230. file prior to writing the data as it does with files opened for RANDOM.
  1231.      As you've seen, data is written to a binary file using the PUT #
  1232. command, and read using GET #.  These work much like their random access
  1233. counterparts in that a seek offset is optional, and if omitted must be
  1234. replaced with an empty comma placeholder.  But where the seek argument in a
  1235. random GET # or PUT # specifies a record number, a binary GET # treats it
  1236. as a byte offset into the file.
  1237.      The first byte in a binary file is considered by BASIC to be byte
  1238. number 1.  This is important to point out now, because DOS considers the
  1239. first byte to be numbered 0.  When we discuss using CALL Interrupt to
  1240. access files in Chapter 12, you will need to take this difference into
  1241. account.
  1242.      When reading and writing binary files, BASIC always uses the length of
  1243. the specified variable to know how many bytes to read or write.  The
  1244. statement GET #1, , IntVar% reads two bytes at the current DOS seek
  1245. location into the integer variable IntVar%, and PUT #1, 1000, LongVar#
  1246. writes the contents of LongVar# (eight bytes) to the file starting at the
  1247. 1000th byte.  Let's now take a look at a practical application of binary
  1248. file techniques.
  1249.      Rather than invent a binary file format as an example, I will instead
  1250. use the Lotus 1-2-3 file structure to illustrate the effective use of
  1251. binary access.  Although it is possible to skip around in a binary file and
  1252. read its data in any arbitrary order, a Lotus worksheet file is intended to
  1253. be read sequentially.  Each data item is preceded by an integer code that
  1254. indicates the type and length of the data that follows.  Note that the same
  1255. format is used by Lotus 1-2-3 versions 1 and 2, and also Lotus Symphony. 
  1256. Newer versions of 1-2-3 that support three-dimensional work sheets use a
  1257. different format that this program will not accommodate.
  1258.      A Lotus spreadsheet can contain as many as 63 different kinds of data. 
  1259. However, we will concern ourselves with only those that are of general
  1260. interest such as cell contents and simple formatting commands.  These are
  1261. Beginning of File, End of File, Integer values, Floating point values, Text
  1262. labels and their format, and the double precision values embedded within a
  1263. Formula record.  The format used by the actual formulas is quite complex,
  1264. and will not be addressed.  Other records that will not be covered here are
  1265. those that pertain to the structure of the worksheet itself.  For example,
  1266. range names, printer setup strings, macro definitions, and so forth.  You
  1267. can get complete information on the Lotus file structure as well as other
  1268. standard formats in Jeff Walden's excellent book, *File Formats for Popular
  1269. PC Software* (Wiley Press, ISBN 0-471-83671-0).  [Unfortunately that book
  1270. is now out of print.  But you may be able to get this information from
  1271. Lotus directly.]
  1272.      A Lotus file is comprised of individual records, and each record may
  1273. have a varying length.  The length of a record depends on its type and
  1274. contents, and most records contain a fixed-length header which describes
  1275. the information that follows.  Regardless of the type of record being
  1276. considered, each follows the same format: an operation code (opcode), the
  1277. data length, and the data itself.
  1278.      The opcode is always a two-byte integer which identifies the type of
  1279. data that will follow.  For example, an opcode of 15 indicates that the
  1280. data in the record will be treated by 1-2-3 as a text label.  The length is
  1281. also an integer, and it holds the number of bytes in the Data section (the
  1282. actual text) that follows.
  1283.      All of the records that pertain to a spreadsheet cell contain a
  1284. five-byte header at the beginning of the data section.  These five bytes
  1285. are included as part of the data's length word.  The first header byte
  1286. contains the formatting information, such as the number of decimal
  1287. positions to display.  The next two bytes together contain the cell's row
  1288. as an integer, and the following two bytes hold the cell's column.
  1289.      Again, this header is present only in records that refer to a cell's
  1290. contents.  For example, the Beginning of File and End of File records do
  1291. not contain a header, nor do those records that describe the worksheet. 
  1292. Some records such as labels and formulas will have a varying length, while
  1293. those that contain numbers will be fixed, depending on the type of number. 
  1294. Floating point values are always eight bytes long, and are in the same IEEE
  1295. format used by BASIC.  Likewise, an integer value will always have a length
  1296. of two bytes.  Because the length word includes the five-byte header size,
  1297. the total length for these double precision and integer examples is 13 and
  1298. 7 respectively.
  1299.      It is important to understand that in a Lotus worksheet file, rows and
  1300. columns are based at zero.  Even though 1-2-3 considers the leftmost row to
  1301. be number 1, it is stored in the file as a zero.  Likewise, the first
  1302. column as displayed by 1-2-3 is labelled "A", but is identified in the file
  1303. as column 0.  Thus, it is up to your program to take that into account as
  1304. translates the columns to the alphabetic format, if you intend to display
  1305. them as Lotus does.
  1306.      In the Read portion of the program that follows, the same steps are
  1307. performed for each record.  That is, binary GET # statements read the
  1308. record's type, length, and data.  If the record type indicates that it
  1309. pertains to a worksheet cell, then the five-byte header is also read using
  1310. the GetFormat subprogram.  Opcodes that are not supported by this program
  1311. are simply displayed, so you will see that they were encountered.
  1312.      The Write portion of the program performs simple formatting, and also
  1313. ensures that a column-width record is written only once.  Table 6-2 shows
  1314. the makeup of the numeric formatting byte used in all Lotus files.
  1315.  
  1316.  
  1317.             bits --> 7  6  5  4  3  2  1  0
  1318.                      ^  ^  ^  ^  ^  ^  ^  ^
  1319.                      |  |  |  |  |  |  |  |
  1320. protected if set ----+  |  |  |  |  |  |  |
  1321.   type of format -------+--+--+  |  |  |  |
  1322. number of digits ----------------+--+--+--+
  1323.  
  1324.                         ^  ^  ^
  1325.                         |  |  |
  1326. fixed number of digits  0  0  0
  1327.   exponential notation  0  0  1
  1328.               currency  0  1  0
  1329.                percent  0  1  1
  1330.     flag to add commas  1  0  0
  1331.                 unused  1  0  1
  1332.                 unused  1  1  0
  1333.           other format  1  1  1
  1334.  
  1335. Table 6-2: The Structure of a Lotus 1-2-3 Format Byte.
  1336.  
  1337.  
  1338. The program example below can either read or write a Lotus 1-2-3 worksheet
  1339. file.  If you select Create when this program is run, it will write a
  1340. worksheet file named SAMPLE.WKS suitable for reading into any version of
  1341. Lotus 123.  This sample file contains an assortment of labels and values. 
  1342. If you select Read, the program will prompt for the name of a worksheet
  1343. file which it then reads and displays.  
  1344.  
  1345. DEFINT A-Z
  1346. DECLARE SUB GetFormat (Format, Row, Column)
  1347. DECLARE SUB WriteColWidth (Column, ColWidth)
  1348. DECLARE SUB WriteInteger (Row, Column, ColWidth, Temp)
  1349. DECLARE SUB WriteLabel (Row, Column, ColWidth, Msg$)
  1350. DECLARE SUB WriteNumber (Row, Col, ColWidth, Fmt$, Num#)
  1351.  
  1352. DIM SHARED CellFmt AS STRING * 1    'to read one byte
  1353. DIM SHARED ColNum(40)               'max columns to write
  1354. DIM SHARED FileNum                  'the file number to use
  1355.  
  1356. CLS
  1357. PRINT "Read an existing 123 file or ";
  1358. PRINT "Create a sample file (R/C)? "
  1359. LOCATE , , 1
  1360. DO
  1361.    X$ = UCASE$(INKEY$)
  1362. LOOP UNTIL X$ = "R" OR X$ = "C"
  1363. LOCATE , , 0
  1364. PRINT X$
  1365.  
  1366. IF X$ = "R" THEN
  1367.  
  1368.   '----- read an existing file
  1369.   INPUT "Lotus file to read: ", FileName$
  1370.   IF INSTR(FileName$, ".") = 0 THEN
  1371.     FileName$ = FileName$ + ".WKS"
  1372.   END IF
  1373.   PRINT
  1374.  
  1375.   '----- get the next file number and open the file
  1376.   FileNum = FREEFILE
  1377.   OPEN FileName$ FOR BINARY AS #FileNum
  1378.  
  1379.   DO UNTIL Opcode = 1       'until End of File code
  1380.  
  1381.      GET FileNum, , Opcode  'get the next opcode
  1382.      GET FileNum, , Length  'and the data length
  1383.  
  1384.      SELECT CASE Opcode     'filter the Opcodes
  1385.  
  1386.     CASE 0                  'Beginning of File record
  1387.       PRINT "Beginning of file, Lotus ";
  1388.       GET FileNum, , Temp
  1389.  
  1390.       SELECT CASE Temp
  1391.         CASE 1028
  1392.           PRINT "1-2-3 version 1.0 or 1A"
  1393.         CASE 1029
  1394.           PRINT "Symphony version 1.0"
  1395.         CASE 1030
  1396.           PRINT "123 version 2.x"
  1397.         CASE ELSE
  1398.           PRINT "NOT a Lotus File!"
  1399.       END SELECT
  1400.  
  1401.     CASE 1                  'End of File
  1402.       PRINT "End of File"
  1403.  
  1404.     CASE 12                 'Blank cell
  1405.        'Note that Lotus saves blank cells only if
  1406.        'they are formatted or protected.
  1407.        CALL GetFormat(Format, Row, Column)
  1408.        PRINT "Blank:      Format ="; Format,
  1409.        PRINT "Row ="; Row,
  1410.        PRINT "Col ="; Column
  1411.  
  1412.     CASE 13                 'Integer
  1413.        CALL GetFormat(Format, Row, Column)
  1414.        GET FileNum, , Temp
  1415.        PRINT "Integer:    Format ="; Format,
  1416.        PRINT "Row ="; Row,
  1417.        PRINT "Col ="; Column,
  1418.        PRINT "Value ="; Temp
  1419.  
  1420.     CASE 14                 'Floating point
  1421.        CALL GetFormat(Format, Row, Column)
  1422.        GET FileNum, , Number#
  1423.        PRINT "Number:     Format ="; Format,
  1424.        PRINT "Row ="; Row,
  1425.        PRINT "Col ="; Column,
  1426.        PRINT "Value ="; Number#
  1427.  
  1428.     CASE 15                 'Label
  1429.        CALL GetFormat(Format, Row, Column)
  1430.        'Create a string to hold the label.  6 is
  1431.        'subtracted to exclude the Format, Column,
  1432.        'and Row information.
  1433.  
  1434.        Info$ = SPACE$(Length - 6)
  1435.        GET FileNum, , Info$         'read the label
  1436.        GET FileNum, , CellFmt$      'eat the CHR$(0)
  1437.        PRINT "Label:      Format ="; Format,
  1438.        PRINT "Row ="; Row,
  1439.        PRINT "Col ="; Column, Info$
  1440.  
  1441.     CASE 16                 'Formula
  1442.        CALL GetFormat(Format, Row, Column)
  1443.        GET FileNum, , Number#      'read cell value
  1444.        GET FileNum, , Length       'and formula length
  1445.        SEEK FileNum, SEEK(FileNum) + Length 'skip formula
  1446.        PRINT "Formula:    Format ="; Format,
  1447.        PRINT "Row ="; Row,
  1448.        PRINT "Col ="; Column,
  1449.        PRINT "Value ="; Number#
  1450.  
  1451.     CASE ELSE
  1452.        Dummy$ = SPACE$(Length)     'skip the record
  1453.        GET FileNum, , Dummy$       'read it in
  1454.        PRINT "Opcode: "; Opcode    'show its Opcode
  1455.  
  1456.      END SELECT
  1457.  
  1458.      '----- pause when the screen fills
  1459.      IF CSRLIN > 21 THEN
  1460.        PRINT
  1461.        PRINT "Press <ESC> to end or ";
  1462.        PRINT "any other key for more"
  1463.        DO
  1464.          K$ = INKEY$
  1465.        LOOP UNTIL LEN(K$)
  1466.        IF K$ = CHR$(27) THEN EXIT DO
  1467.        CLS
  1468.      END IF
  1469.  
  1470.      NumRecs = NumRecs + 1      'count the records
  1471.  
  1472.   LOOP
  1473.   PRINT "Number of Records Processed ="; NumRecs
  1474.   CLOSE
  1475.  
  1476. ELSE
  1477.  
  1478.   '----- write a sample file
  1479.   FileNum = FREEFILE            'as above
  1480.   OPEN "SAMPLE.WKS" FOR BINARY AS #FileNum
  1481.  
  1482.   Temp = 0                      'OpCode for Start of File
  1483.   PUT FileNum, , Temp           'write that
  1484.   Temp = 2                      'its data length is 2
  1485.   PUT FileNum, , Temp           'since it's an integer
  1486.   Temp = 1030                   'Lotus version 2.x
  1487.   PUT FileNum, , Temp
  1488.  
  1489.   Row = 0                       'write this in Row 1
  1490.   DO
  1491.      CALL WriteLabel(Row, 0, 16, "This is a Label")
  1492.      CALL WriteLabel(Row, 1, 12, "So is this")
  1493.      CALL WriteInteger(Row, 2, 7, 12345)
  1494.      CALL WriteNumber(Row, 3, 9, "C2", 57.23#)
  1495.      CALL WriteNumber(Row, 4, 9, "F5", 12.3456789#)
  1496.      CALL WriteInteger(Row, 6, 9, 99)  'skip a column for fun
  1497.      Row = Row + 1                     'go on to the next row
  1498.   LOOP WHILE Row < 6
  1499.  
  1500.   '----- Write the End of File record and close the file
  1501.   Temp = 1                  'Opcode for End of File
  1502.   PUT FileNum, , Temp
  1503.   Temp = 0                  'the data length is zero
  1504.   PUT FileNum, , Temp
  1505.   CLOSE
  1506.  
  1507. END IF
  1508. END
  1509.  
  1510. SUB GetFormat (Format, Row, Column) STATIC
  1511.   GET FileNum, , CellFmt$: Format = ASC(CellFmt$)
  1512.   GET FileNum, , Column
  1513.   GET FileNum, , Row
  1514. END SUB
  1515.  
  1516. SUB WriteColWidth (Column, ColWidth) STATIC
  1517.  
  1518.   '----- allow a column width only once for each column
  1519.   IF NOT ColNum(Column) THEN
  1520.     Temp = 8
  1521.     PUT FileNum, , Temp
  1522.     Temp = 3
  1523.     PUT FileNum, , Temp
  1524.     PUT FileNum, , Column
  1525.     Temp$ = CHR$(ColWidth)
  1526.     PUT FileNum, , Temp$
  1527.     '----- show we wrote this column's width
  1528.     ColNum(Column) = -1
  1529.   END IF
  1530.  
  1531. END SUB
  1532.  
  1533. SUB WriteInteger (Row, Column, ColWidth, Integ) STATIC
  1534.  
  1535.   Temp = 13                     'OpCode for an integer
  1536.   PUT FileNum, , Temp
  1537.   Temp = 7                      'Length + 5 byte header
  1538.   PUT FileNum, , Temp
  1539.   Temp$ = CHR$(127)             'the format portion
  1540.   PUT FileNum, , Temp$
  1541.   PUT FileNum, , Column
  1542.   PUT FileNum, , Row
  1543.   PUT FileNum, , Integ
  1544.   CALL WriteColWidth(Column, ColWidth)
  1545.  
  1546. END SUB
  1547.  
  1548. SUB WriteLabel (Row, Column, ColWidth, Msg$)
  1549.  
  1550.   IF LEN(Msg$) > 240 THEN       '240 is the maximum length
  1551.     Msg$ = LEFT$(Msg$, 240)
  1552.   END IF
  1553.  
  1554.   Temp = 15                     'OpCode for a label
  1555.   PUT FileNum, , Temp
  1556.   Temp = LEN(Msg$) + 7          'Length plus 5-byte header
  1557.                                 'plus "'" plus CHR$(0)
  1558.   PUT FileNum, , Temp
  1559.   Temp$ = CHR$(127)             '127 is the default format
  1560.   PUT FileNum, , Temp$
  1561.   PUT FileNum, , Column
  1562.   PUT FileNum, , Row
  1563.   Temp$ = "'" + Msg$ + CHR$(0)  'a "'" left-aligns a label
  1564.                                 'use "^" instead to center
  1565.   PUT FileNum, , Temp$
  1566.   CALL WriteColWidth(Column, ColWidth)
  1567.  
  1568. END SUB
  1569.  
  1570. SUB WriteNumber (Row, Col, ColWidth, Fmt$, Num#) STATIC
  1571.  
  1572.   IF LEFT$(Fmt$, 1) = "F" THEN                    'fixed
  1573.     '----- specify the number of decimal places
  1574.      Format$ = CHR$(0 + VAL(RIGHT$(Fmt$, 1)))
  1575.   ELSEIF LEFT$(Fmt$, 1) = "C" THEN                'currency
  1576.      Format$ = CHR$(32 + VAL(RIGHT$(Fmt$, 1)))
  1577.   ELSEIF LEFT$(Fmt$, 1) = "P" THEN                'percent
  1578.      Format$ = CHR$(48 + VAL(RIGHT$(Fmt$, 1)))
  1579.   ELSE                                            'default
  1580.      Format$ = CHR$(127)    'use CHR$(255) for protected
  1581.   END IF
  1582.  
  1583.   Temp = 14                 'Opcode for a number
  1584.   PUT FileNum, , Temp
  1585.   Temp = 13                 'Length (8) + 5 = 13
  1586.   PUT FileNum, , Temp
  1587.  
  1588.   PUT FileNum, , Format$
  1589.   PUT FileNum, , Col
  1590.   PUT FileNum, , Row
  1591.   PUT FileNum, , Num#
  1592.  
  1593.   CALL WriteColWidth(Column, ColWidth) 
  1594.  
  1595. END SUB
  1596.  
  1597. There are several points worth noting about this program.  First, Lotus
  1598. label strings are always terminated with a CHR$(0) zero byte, which is the
  1599. same method used by DOS and the C language.  Therefore, the WriteLabel
  1600. subprogram adds this byte, which is also included as part of the length
  1601. word that follows the Opcode.
  1602.      In the WriteNumber subprogram, the 1-byte format code is either 127 to
  1603. default to unformatted, or bit-coded to indicate fixed, currency, or
  1604. percent formatting.  WriteNumber expects a format string such as "F3" which
  1605. indicates fixed-point with three decimal positions, or "P1" for percent
  1606. formatting using one decimal place.  If you instead use "C", WriteNumber
  1607. will use a fixed 2-decimal point currency format.
  1608.      Earlier I pointed out the extra work is needed to write a constant
  1609. value to a binary file, because only variables may be used with PUT #. 
  1610. This is painfully clear in each of the Write subprograms, where the integer
  1611. variable Temp is repeatedly assigned to new values.  We can only hope that
  1612. Microsoft will see fit to remove this arbitrary limitation in a later
  1613. version of BASIC.
  1614.      Finally, note the use of the fixed-length string CellFmt$.  Although
  1615. some language support a one-byte numeric variable type, BASIC does not. 
  1616. Therefore, to read and write these values you must use a fixed-length
  1617. string.  To determine the value after reading a file you will use ASC, and
  1618. to assign a value prior to writing it you instead use CHR$.  For example,
  1619. to assign CellFmt$ to the byte value 123 use CellFmt$ = CHR$(123).
  1620.  
  1621.  
  1622. NAVIGATING YOUR FILES
  1623.  
  1624. BASIC offers a number of file-related functions to determine how long a
  1625. file is, the current DOS seek location where the next read or write will
  1626. take place, and also if that location is at the end of the file.  These are
  1627. LOF, LOC and SEEK, and EOF respectively.  LOF stands for Length Of File,
  1628. LOC means current Location, and EOF is End Of File.  The SEEK statement is
  1629. also available to force the next file access to occur at a specified place
  1630. within the file.  All of these require a file number argument to indicate
  1631. which file is being referred to.
  1632.  
  1633.  
  1634. The EOF Function
  1635.  
  1636. The EOF function is most useful when reading sequential text files, and it
  1637. avoids BASIC's "Input past end" error that would otherwise result from
  1638. trying to read past the end of the available data.  The following short
  1639. complete program reads a text file and displays it contents, and shows how
  1640. EOF is used for this purpose.
  1641.  
  1642.  
  1643. OPEN FileName$ FOR INPUT AS #1
  1644.   WHILE NOT EOF(1)
  1645.     LINE INPUT #1, This$
  1646.     PRINT This$
  1647.   WEND
  1648. CLOSE
  1649.  
  1650.  
  1651. Notice the use of the NOT operator in this example.  The EOF function
  1652. returns an integer value of either -1 or 0, to indicate true (at the end of
  1653. the file) or false.  Therefore, NOT -1 is equal to 0 (False), and NOT 0 is
  1654. equal to -1 (True).  This use of bit manipulation was described earlier in
  1655. Chapter 2.
  1656.      EOF can also be used with binary and random access files for the same
  1657. purpose.  In fact, EOF may be even more useful in those cases, because
  1658. BASIC does not create an error when you attempt to read past the end as it
  1659. does for sequential files.  Indeed, once you go past the end of a binary or
  1660. random access file, BASIC simply fills the variables being read with zero
  1661. bytes.  Without EOF there is no way to distinguish between zeros returned
  1662. by BASIC because you went past the end of the file and zeros that were read
  1663. as legitimate data.
  1664.      The EOF function was originally needed with DOS 1.0 for a program to
  1665. determine when the end of the file was reached.  That version of DOS always
  1666. wrote all data in multiples of 128 bytes, and all file directory entries
  1667. also were listed with lengths being a multiple of 128.  [That is, a file
  1668. which contains only ten bytes of data will be reported by DIR as being 128
  1669. bytes long.]  To indicate the true end of the file, a CHR$(26) end of file
  1670. marker was placed just past the last byte of valid data.  Thus, EOF was
  1671. originally written to search for a byte with that value, and return True
  1672. when it was found.
  1673.      Most modern applications do not use an EOF character, and instead rely
  1674. on the file length that is stored in the file's directory entry.  However,
  1675. some older programs still write a CHR$(26) at the end of the data, and DOS'
  1676. COPY CON command does this as well.  Therefore, BASIC's EOF will return a
  1677. True value when this character is encountered, even if there is still more
  1678. data to be read in the file.  In fact, you can provide a minimal amount of
  1679. data security by intentionally writing a CHR$(26) at or near the beginning
  1680. of a sequential file.  If someone then uses the DOS TYPE command to view
  1681. the file, only what precedes the EOF marker will be displayed.
  1682.      Another implication of EOF characters in BASIC surfaces when you open
  1683. a sequential file for append mode.  BASIC makes a minimal attempt to locate
  1684. an EOF character, and if one exists it begins appending on top of it. 
  1685. After all, if writing started just past the EOF byte, a subsequent LINE
  1686. INPUT would fail when it reached that point.  Likewise, an EOF test would
  1687. return true and the program would stop reading at that location in the
  1688. file.  Therefore, BASIC checks the last few bytes in the file when you open
  1689. for append, to see if an EOF marker is present.  However, if the marker is
  1690. much earlier in a large file, BASIC will not see it.
  1691.      When EOF is used with serial communications, it returns 0 until a
  1692. CHR$(26) byte is received, at which point it continues to return -1 until
  1693. the communications port is closed.
  1694.  
  1695.  
  1696. The LOF Function
  1697.  
  1698. The LOF function simply returns the current length of the file, and that
  1699. too can be used as a way to tell when you have reached the end.  In the
  1700. random access FIELD example program shown earlier, LOF was used in
  1701. conjunction with the record length to determine the number of records in
  1702. the file.  Since the length of most random access files is directly related
  1703. to [and evenly divisible by] the number of records in the file, simple
  1704. division can be used to determine how many records there are.  The formula
  1705. is NumRecords = LOF(FileNum) \ RecLength.
  1706.      Understand that when used with sequential and binary files, LOF
  1707. returns the length of the file in bytes.  But with a random access file,
  1708. LOF instead provides the number of records.
  1709.      LOF can also be used as a crude way to see if a file exists.  Even
  1710. though this is done much more effectively and elegantly with assembly
  1711. language or CALL Interrupt, the short example below shows how LOF can be
  1712. used for this purpose.
  1713.  
  1714.  
  1715. FUNCTION Exist% (FileName$) STATIC
  1716.   FileNum = FREEFILE
  1717.   OPEN FileName$ FOR BINARY AS #FileNum
  1718.     Length = LOF(FileNum)
  1719.   CLOSE #FileNum
  1720.   IF Length = 0 THEN   'it probably wasn't there
  1721.     Exist% = 0         'return False to show that
  1722.     KILL FileName$     'and delete what we created
  1723.   ELSE
  1724.     Exist% = -1        'otherwise return True
  1725.   END IF
  1726. END FUNCTION
  1727.  
  1728.  
  1729. Besides being clunky, this program also has a serious flaw: If the file
  1730. does exist but has a perfectly legal length of zero, this function will say
  1731. it doesn't exist and then delete it!  As I said, this method is crude, but
  1732. a lot of programmers have used it.
  1733.  
  1734.  
  1735. The LOC and SEEK Functions
  1736.  
  1737. LOC and SEEK are closely related, in that they return information about
  1738. where you are in the file.  However, LOC reports the position of the last
  1739. read or write, and SEEK tells where the next one will occur.  As with LOF,
  1740. LOC and SEEK return byte values for files that were opened for sequential
  1741. or binary access, and record numbers when used with random access files.
  1742.      In practice, LOC is of little value, especially when you are
  1743. manipulating sequential files.  For reasons that only Microsoft knows, LOC
  1744. returns the number of the last byte read or written, but *divided by 128*. 
  1745. Since no program I know of treats sequential files as containing 128-byte
  1746. records, I cannot imagine how this could be useful.  Further, since LOC
  1747. returns the location of the *last* read or write, it never reflects the
  1748. true position in the file.
  1749.      When used with communications, LOC reports the number of characters in
  1750. the receive buffer that are currently waiting to be read, which is useful. 
  1751. When used with INPUT$ #, LOC provides a handy way to retrieve all of the
  1752. characters present in the buffer at one time.  This is shown in context
  1753. below, and the example assumes that the communications port has already
  1754. been opened.
  1755.  
  1756.  
  1757. NumChars = LOC(1)
  1758. IF NumChars THEN
  1759.   This$ = INPUT$(NumChars)
  1760. END IF
  1761.  
  1762.  
  1763. The SEEK function always returns the current file position, which is the
  1764. point at which the next read or write will take place.  One good use for
  1765. SEEK is to read the current location in a sequential file, to allow a
  1766. program to walk backwards through the file later.  For example, if you need
  1767. to create a text file browsing program, there is no other way to know where
  1768. the previous line of a file is located.  A short program that shows this in
  1769. context follows in the section that describes the SEEK statement.
  1770.  
  1771.  
  1772. The SEEK Statement
  1773.  
  1774. Where the SEEK function lets you determine where you are currently in a
  1775. file, the SEEK statement lets you move to any arbitrary position.  As you
  1776. might imagine, SEEK as a statement is similar to the function version in
  1777. that it assumes a byte value when used with sequential and binary files,
  1778. and a record number with random access files.
  1779.      SEEK can be very useful in a variety of situations, and in particular
  1780. when indexing random access files.  When an indexing system is employed,
  1781. selected portions of a data file are loaded into memory where they can be
  1782. searched very quickly.  Since the location of the index information being
  1783. searched corresponds to the record number of the complete data record, the
  1784. record can be accessed with a single GET #.  This was described briefly in
  1785. the discussion of the BASIC PDS ISAM options in Chapter 5.  Thus, once the
  1786. record number for a given entry has been identified, the SEEK statement (or
  1787. the SEEK argument in the GET # command) is used to access that particular
  1788. record.
  1789.      For this example, though, I will instead show how SEEK can be used
  1790. with a sequential file.  The following complete program provides the
  1791. rudiments of a text file browser, but this version displays only one line
  1792. at a time.  It would be fairly easy to expand this program to display
  1793. entire screenfuls of text, and I leave that as an exercise for you.
  1794.      The program begins by prompting for a file name, and then opens that
  1795. file for sequential input.  The maximum number of lines that can be
  1796. accommodated is set arbitrarily at 5000, though you will not be able to
  1797. specify more than 16384 unless you compile with the /ah option.  The long
  1798. integer Offset&() array is used to remember where each line encountered so
  1799. far in the file begins, and 16384 is the maximum number of elements that
  1800. can fit into a single 64K array.  For a typical text file with line lengths
  1801. that average 60 characters, 16384 lines is nearly 1MB of text.
  1802.      When you run the program, it expects only the up and down arrow keys
  1803. to advance and go backwards through the file, the Home key to jump to the
  1804. beginning, or the Escape key to end the program.  Notice that the words
  1805. "blank line" are printed when a blank line is encountered, just so you can
  1806. see that something has happened.
  1807.  
  1808. DEFINT A-Z
  1809. CONST MaxLines% = 5000
  1810. REDIM Offset&(1 TO MaxLines%)
  1811.  
  1812. CLS
  1813. PRINT "Enter the name of file to browse: ";
  1814. LINE INPUT "", FileName$
  1815.  
  1816. OPEN FileName$ FOR INPUT AS #1
  1817.  
  1818.   Offset&(1) = 1                'initialize to offset 1
  1819.   CurLine = 1                   'and start with line 1
  1820.  
  1821.   WHILE Action$ <> CHR$(27)     'until they press Escape
  1822.     SEEK #1, Offset&(CurLine)   'seek to the current line
  1823.     LINE INPUT #1, Text$        'read that line
  1824.     Offset&(CurLine + 1) = SEEK(1)  'save where the next
  1825.                                     '  line starts
  1826.     CLS
  1827.     IF LEN(Text$) THEN          'if it's not blank
  1828.       PRINT Text$               'print the line
  1829.     ELSE                        'otherwise
  1830.       PRINT "(blank line)"      'show that it's blank
  1831.     END IF
  1832.  
  1833.     DO                          'wait for a key
  1834.       Action$ = INKEY$
  1835.     LOOP UNTIL LEN(Action$)
  1836.  
  1837.     SELECT CASE ASC(RIGHT$(Action$, 1))
  1838.       CASE 71                   'Home
  1839.         CurLine = 1
  1840.  
  1841.       CASE 72                   'Up arrow
  1842.         IF CurLine > 1 THEN
  1843.           CurLine = CurLine - 1
  1844.         END IF
  1845.  
  1846.       CASE 80                   'Down arrow
  1847.         IF (NOT EOF(1)) AND CurLine < MaxLines% THEN
  1848.           CurLine = CurLine + 1
  1849.         END IF
  1850.  
  1851.       CASE ELSE
  1852.     END SELECT
  1853.   WEND
  1854. CLOSE
  1855. END
  1856.  
  1857. You should be aware that BASIC does not prevent you from using SEEK to go
  1858. past the end of a file that has been opened for Binary access.  If you do
  1859. this and then write any data, DOS will actually extend the file to include
  1860. the data that was just written.  Therefore, it is important to understand
  1861. that any data that lies between the previous end of the file and the newly
  1862. added data will be undefined.  When a file is deleted DOS simply abandons
  1863. the sectors that held its data, and makes them available for later use. 
  1864. But whatever data those sectors contained remains intact.  When you later
  1865. expand a file this way using SEEK, the old abandoned sector contents are
  1866. incorporated into the file.  Even if the sectors that are allocated were
  1867. never written to previously, they will contain the &HF6 bytes that DOS'
  1868. FORMAT.COM uses to initialize a disk.
  1869.      You can turn this behavior into an important feature, and in some
  1870. cases recreate a file that was accidentally truncated.  If you erase a file
  1871. by mistake, it is possible to recover it using the Norton Utilities or a
  1872. similar disk utility program.  But when an existing file is opened for
  1873. output, DOS truncates it to a length of zero.  The following program shows
  1874. the steps necessary to reconstruct a file that has been destroyed this way.
  1875.  
  1876.  
  1877. OPEN FileName$ FOR BINARY AS #1
  1878. SEEK #1, 30000
  1879. PUT #1, , X%
  1880. CLOSE #1
  1881.  
  1882.  
  1883. In this case, the file is restored to a length of 30000, and you can use
  1884. larger or smaller values as appropriate.  Understand that there is no
  1885. guarantee that DOS will reassign the same sectors to the file that it
  1886. originally used.  But I have seen this trick work more than once, and it is
  1887. at least worth a try.
  1888.      In a similar fashion, you can reduce the size of a file by seeking to
  1889. a given location and then writing *zero* bytes there.  Since BASIC provides
  1890. no way to write zero bytes to a file, some additional trickery is needed. 
  1891. This will be described in Chapter 12 in the section that discusses using
  1892. CALL Interrupt to access DOS and BIOS services.
  1893.  
  1894.  
  1895. ADVANCED FILE TECHNIQUES
  1896. ========================
  1897.  
  1898. There are a number of clever file-related tricks that can be performed
  1899. using only BASIC programming.  Some of these tricks help you to improve on
  1900. BASIC's speed, and others let you do things that are not possible using the
  1901. normal and obvious methods.  BASIC is no slower than other languages when
  1902. reading and writing large amounts of data, and indeed, the bottleneck is
  1903. frequently DOS itself.  Further, if you can reduce the amount of data that
  1904. is written, your files will be smaller as well.  With that in mind, let's
  1905. look at some ways to further improve your programs.
  1906.  
  1907.  
  1908. SPEEDING UP FILE ACCESS
  1909.  
  1910. The single most important way to speed up your programs is to read and
  1911. write large amounts of data in one operation.  The normal method for saving
  1912. a numeric or TYPE array is to write each element to disk in a loop.  But
  1913. when there are many thousands of elements, a substantial amount of overhead
  1914. is incurred just from BASIC's repeated calls to DOS.  There are several
  1915. solutions you can consider, each with increasing levels of complexity.
  1916.  
  1917.  
  1918. BLOAD and BSAVE
  1919.  
  1920. The simplest way to read and write a large amount of contiguous data is
  1921. with BLOAD and BSAVE.  BSAVE takes a "snapshot" of any contiguous area of
  1922. memory up to 64K in size, and saves it to disk in a single operation.  When
  1923. an application calls DOS to read or write a file, it furnishes DOS with the
  1924. segment and address where the data is to be loaded or saved from, and also
  1925. the number of bytes.  BLOAD and BSAVE provide a simple interface to the DOS
  1926. read and write services, and they can be used to load and save numeric
  1927. arrays up to 64K in size, as well as screen images.
  1928.      [I have seen a number of messages in the MSBASIC forum on CompuServe
  1929. stating that BSAVE and BLOAD do not work with compressed disks.  Many of
  1930. those messages have come from Microsoft technical support, and I have no
  1931. reason to doubt them.  It may be that only VB/DOS has this problem, but I
  1932. have no way to test QB and PDS because I don't use disk compression.]
  1933.      A file that has been written using BSAVE includes a 7-byte header that
  1934. identifies it as a BSAVE file, and also shows where it was saved from and
  1935. how many bytes it contains.  BLOAD requires this header, and thus cannot be
  1936. used with any arbitrary type of file.  But when used together, these
  1937. commands can be as much as ten times faster than a FOR/NEXT loop.
  1938.      The example below creates and then saves a single precision array, and
  1939. then loads it again to prove the process worked.
  1940.  
  1941.  
  1942. DEFINT A-Z
  1943. CONST NumEls% = 20000
  1944. REDIM Array(1 TO NumEls%)            'create the array
  1945.  
  1946. FOR X = 1 TO NumEls%                 'file it with values
  1947.   Array(X) = X
  1948. NEXT
  1949.  
  1950. DEF SEG = VARSEG(Array(1))           'set the BSAVE segment
  1951. BSAVE "ARRAY.DAT", VARPTR(Array(1)), NumEls% * LEN(Array(1))
  1952.  
  1953. REDIM Array(1 TO NumEls%)            'recreate the array
  1954. DEF SEG = VARSEG(Array(1))           'the array may have moved
  1955. BLOAD "ARRAY.DAT", VARPTR(Array(1))
  1956.  
  1957. FOR X = 1 TO NumEls%                 'prove the data is valid
  1958.   IF Array(X) <> X THEN
  1959.     PRINT "Error in element"; X
  1960.   END IF
  1961. NEXT
  1962. END
  1963.  
  1964.  
  1965. Because BSAVE and BLOAD use the current DEF SEG setting to know the segment
  1966. the data is in, VARSEG is used with the first element of the array.  Once
  1967. the correct segment has been established, BSAVE is given the name of the
  1968. file to save, the starting address, and the number of bytes of data.  As
  1969. with the TYPE variable example shown earlier, LEN is ideal here as well to
  1970. help calculate the number of bytes that must be saved.  In this case, each
  1971. integer array element is two bytes long, and BASIC multiplies the constants
  1972. NumEls% and LEN(Array(1)) when the program is compiled.  Therefore, no
  1973. additional code is added to the program to calculate this value at runtime.
  1974.      Once the array has been saved it is redimensioned, which effectively
  1975. clears it to all zero values prior to reloading.  Notice that DEF SEG is
  1976. used again before the BLOAD statement.  This is an important point, because
  1977. there is no guarantee that BASIC will necessarily allocate the same block
  1978. of memory the second time.  If a file is loaded into the wrong area of
  1979. memory, your program is sure to crash or at least not work correctly.
  1980.      Also note that BLOAD always loads the entire file, and a length
  1981. argument is not needed or expected.  This brings up an important issue: how
  1982. can you determine how large to dimension an array prior to loading it?  The
  1983. answer, as you may have surmised, is to open the file for binary access and
  1984. read the length stored in the BSAVE header.  All that's needed is to know
  1985. how the header is organized, as the following program reveals.
  1986.  
  1987. DEFINT A-Z
  1988. TYPE BHeader
  1989.   Header AS STRING * 1
  1990.   Segment AS INTEGER
  1991.   Address AS INTEGER
  1992.   Length AS INTEGER
  1993. END TYPE
  1994. DIM BLHeader AS BHeader
  1995.  
  1996. OPEN "ARRAY.DAT" FOR BINARY AS #1
  1997.   GET #1, , BLHeader
  1998. CLOSE
  1999.  
  2000. IF ASC(BLHeader.Header) <> &HFD THEN
  2001.   PRINT "Not a valid BSAVE file"
  2002.   END
  2003. END IF
  2004.  
  2005. LongLength& = BLHeader.Length
  2006. IF LongLength& < 0 THEN
  2007.   LongLength& = LongLength& + 65536
  2008. END IF
  2009.  
  2010. NumElements = LongLength& \ 2
  2011. REDIM Array(1 TO NumElements)
  2012.  
  2013. DEF SEG = VARSEG(Array(1))
  2014. BLOAD "ARRAY.DAT", VARPTR(Array(1))
  2015. END
  2016.  
  2017. Even though the original segment and address from which the file was saved
  2018. is in the BSAVE header, that information is not used here.  In most
  2019. situations you will always provide BLOAD with an address to load the file
  2020. to.  However, if the address is omitted, BASIC uses the segment and address
  2021. stored in the file, and ignores the current DEF SEG setting.  This would be
  2022. useful when handling text and graphics images which are always loaded to
  2023. the same segment from which they were originally saved.  But in general I
  2024. recommend that you always define an explicit segment and address.
  2025.      There are a few other points worth elaborating on as well.  First, the
  2026. program examines the first byte in the file to be sure it is the special
  2027. value &HFD which identifies a BSAVE file.  The ASC function is required for
  2028. that, since the only way to define a TYPE component one byte long is as a
  2029. string.
  2030.      Second, the length is stored as an unsigned integer, which cannot be
  2031. manipulated directly in a BASIC program if its value exceeds 32767.  As you
  2032. learned in Chapter 2, integer values larger than 32767 are treated by BASIC
  2033. as signed, and in this case they are considered negative.  Therefore, the
  2034. value is first assigned to a long integer, which is then tested for a value
  2035. less than zero.  If it is indeed negative, 65536 is added to the variable
  2036. to convert it to an equivalent positive number.  Note that the length in a
  2037. BSAVE header does not include the header length; only the data itself is
  2038. considered.
  2039.      If you single-step through this program after running the earlier one
  2040. that created the file, you will see that the code that adds 65536 is
  2041. executed, because the header shows that the file contains 40000 bytes.
  2042.      There are two limitations to using BSAVE and BLOAD this way.  One
  2043. problem is that you may not want the header to be attached to the file. 
  2044. The other, more important problem is that BASIC allows arrays to exceed
  2045. 64K.  Saving a single huge array in multiple files is clumsy, and
  2046. contributes to the clutter on your disks.  The header issue is less
  2047. important, because you can always access the file with normal binary
  2048. statements after using a SEEK to skip over the header.  But the huge array
  2049. problem requires some heavy ammunition.
  2050.      One final point worth mentioning is that BSAVE and BLOAD assume a .BAS
  2051. file name extension if none is given.  This is incredibly stupid, since the
  2052. contents of a BSAVE file have no relationship to a BASIC source file. 
  2053. Therefore, to save a file with no extension at all you must append a period
  2054. to the name: BSAVE "MYFILE.", Address, Length.
  2055.  
  2056.  
  2057. Beyond BSAVE
  2058.  
  2059. The program that follows includes both a demonstration and a pair of
  2060. subprograms that let you save any data regardless of its size or location. 
  2061. These routines are primarily intended for saving huge numeric and TYPE
  2062. arrays, but there is no reason they couldn't be used for other purposes. 
  2063. However, they cannot be used with conventional variable-length string
  2064. arrays, because the data in those arrays is not contiguous.  The file is
  2065. processed in 16K blocks using multiple passes, and the actual saving and
  2066. loading is performed by calling BASIC's internal PUT # and GET # routines.
  2067.  
  2068. DEFINT A-Z
  2069. 'NOTE: This program must be compiled with the /ah option.
  2070.  
  2071. DECLARE SUB BigLoad (FileName$, Segment, Address, Bytes&)
  2072. DECLARE SUB BigSave (FileName$, Segment, Address, Bytes&)
  2073. DECLARE SUB BCGet ALIAS "B$GET3" (BYVAL FileNum, BYVAL Segment, _
  2074.   BYVAL Address, BYVAL NumBytes)
  2075. DECLARE SUB BCPut ALIAS "B$PUT3" (BYVAL FileNum, BYVAL Segment, _
  2076.   BYVAL Address, BYVAL NumBytes)
  2077.  
  2078. CONST NumEls% = 20000
  2079. REDIM Array&(1 TO NumEls%)
  2080. NumBytes& = LEN(Array&(1)) * CLNG(NumEls%)
  2081.  
  2082. FOR X = 1 TO NumEls%            'fill the array
  2083.   Array&(X) = X
  2084. NEXT
  2085.  
  2086. Segment = VARSEG(Array&(1))     'save the array
  2087. Address = VARPTR(Array&(1))
  2088. CALL BigSave("ARRAY.DAT", Segment, Address, NumBytes&)
  2089.  
  2090. REDIM Array&(1 TO NumEls%)      'clear the array
  2091.  
  2092. Segment = VARSEG(Array&(1))     'reload the array
  2093. Address = VARPTR(Array&(1))
  2094. CALL BigLoad("ARRAY.DAT", Segment, Address, NumBytes&)
  2095.  
  2096. FOR X = 1 TO NumEls%            'prove this all worked
  2097.   IF Array&(X) <> X THEN
  2098.     PRINT "Error in element"; X
  2099.   END IF
  2100. NEXT
  2101. END
  2102.  
  2103. SUB BigLoad (FileName$, DataSeg, Address, Bytes&) STATIC
  2104.  
  2105.   FileNum = FREEFILE
  2106.   OPEN FileName$ FOR BINARY AS #FileNum
  2107.   NumBytes& = Bytes&            'work with copies to
  2108.   Segment = DataSeg             'protect the parameters
  2109.  
  2110.   DO
  2111.     IF NumBytes& > 16384 THEN
  2112.       CurrentBytes = 16384
  2113.     ELSE
  2114.       CurrentBytes = NumBytes&
  2115.     END IF
  2116.     CALL BCGet(FileNum, Segment, Address, CurrentBytes)
  2117.     NumBytes& = NumBytes& - CurrentBytes
  2118.     Segment = Segment + &H400
  2119.   LOOP WHILE NumBytes&
  2120.  
  2121.   CLOSE #FileNum
  2122.  
  2123. END SUB
  2124.  
  2125. SUB BigSave (FileName$, DataSeg, Address, Bytes&) STATIC
  2126.  
  2127.   FileNum = FREEFILE
  2128.   OPEN FileName$ FOR BINARY AS #FileNum
  2129.   NumBytes& = Bytes&            'work with copies to
  2130.   Segment = DataSeg             'protect the parameters
  2131.  
  2132.   DO
  2133.     IF NumBytes& > 16384 THEN
  2134.       CurrentBytes = 16384
  2135.     ELSE
  2136.       CurrentBytes = NumBytes&
  2137.     END IF
  2138.     CALL BCPut(FileNum, Segment, Address, CurrentBytes)
  2139.     NumBytes& = NumBytes& - CurrentBytes
  2140.     Segment = Segment + &H400
  2141.   LOOP WHILE NumBytes&
  2142.  
  2143.   CLOSE #FileNum
  2144.  
  2145. END SUB
  2146.  
  2147. Although BASIC lets you save and load only single variables or array
  2148. elements, its internal library routines can work with data of nearly any
  2149. size.  And since TYPE variables can be as large as 64K, these routines must
  2150. be able to accommodate data at least that big.  Therefore, BASIC's usual
  2151. restriction on what you can and cannot read or write to disk with GET # and
  2152. PUT # is an arbitrary one.
  2153.      Accessing BASIC's internal routines requires that you declare them
  2154. using ALIAS, since it is illegal to call a routine that has a dollar sign
  2155. in its name.  As you can see, these routines expect their parameters to be
  2156. passed by value, and this is handled by the DECLARE statements.  Normally,
  2157. you cannot call these routines from within the QB editing environment.  But
  2158. if you separate the two subprograms and place them into a different module,
  2159. that module can be compiled and added to a Quick Library.  That is, the
  2160. subprograms can be together in one file, but not with the demo that calls
  2161. them.  Be sure to add the two DECLARE statements that define B$PUT3 and
  2162. B$GET3 to that module as well.
  2163.      The long integer array this program creates exceeds the normal 64K
  2164. limit, so the /ah compiler switch must be used.  Notice in the BigLoad and
  2165. BigSave subprograms that copies are made of two of the incoming parameters. 
  2166. If this were not done, the subprograms would change the passed values,
  2167. which is a bad practice in this case.  Also, notice how the segment value
  2168. that is used for saving and loading is adjusted through each pass of the DO
  2169. loop.  Since the data is saved in 16K blocks, the segment must be increased
  2170. by 16384 \ 16 = 1024 for each pass.  The use of an equivalent &H value here
  2171. is arbitrary; I translated this program from another version written in
  2172. assembly language that used Hex for that number.
  2173.  
  2174.  
  2175. Processing Large Files
  2176.  
  2177. Although the solutions shown so far are valuable when saving or loading
  2178. large amounts of data, that is as far as they go.  In many cases you will
  2179. also need to process an entire existing file.  Some examples are a program
  2180. that copies or encrypts files, or a routine that searches an entire file
  2181. for a string of text.  As with saving and loading files, processing a file
  2182. or portion of a file in large blocks is always faster and more effective
  2183. than processing it line by line.
  2184.      The file copying subprogram below accepts source and destination file
  2185. names, and copies the data in 4K blocks.  The 4K size is significant,
  2186. because it is large enough to avoid many repeated calls to DOS, and small
  2187. enough to allow a conventional string to be used as a file buffer.  As with
  2188. the BigLoad and BigSave routines, the file is processed in pieces.  Also,
  2189. for simplicity a complete file name and path is required.  Although the DOS
  2190. COPY command lets you use a source file name and a destination drive or
  2191. path only, the CopyFile subprogram requires that entire file names be given
  2192. for both.
  2193.  
  2194. DEFINT A-Z
  2195. DECLARE SUB CopyFile (InFile$, OutFile$)
  2196.  
  2197. SUB CopyFile (InFile$, OutFile$) STATIC
  2198.  
  2199.   File1 = FREEFILE
  2200.   OPEN InFile$ FOR BINARY AS #File1
  2201.  
  2202.   File2 = FREEFILE
  2203.   OPEN OutFile$ FOR BINARY AS #File2
  2204.  
  2205.   Remaining& = LOF(File1)
  2206.   DO
  2207.     IF Remaining& > 4096 THEN
  2208.       ThisPass = 4096
  2209.     ELSE
  2210.       ThisPass = Remaining&
  2211.     END IF
  2212.     Buffer$ = SPACE$(ThisPass)
  2213.     GET #File1, , Buffer$
  2214.     PUT #File2, , Buffer$
  2215.     Remaining& = Remaining& - ThisPass
  2216.   LOOP WHILE Remaining&
  2217.  
  2218.   CLOSE File1, File2
  2219.  
  2220. END SUB
  2221.  
  2222. Once the basic structure of a routine that processes an entire file has
  2223. been established, it can be easily modified for other purposes.  For
  2224. example, CopyFile can be altered to encrypt an entire file, search a file
  2225. for a text string, and so forth.  A few of these will be shown here.  Note
  2226. that for simplicity and clarity, CopyFile creates a new buffer with each
  2227. pass through the loop.  You could avoid that by preceding the assignment
  2228. with IF LEN(Buffer$) <> ThisPass THEN or similar logic, to avoid creating
  2229. the buffer when it already exists and is the correct length.
  2230.      The BufIn function and example below serves as a very fast LINE INPUT
  2231. replacement.  Even though BASIC's own file input routines provide buffering
  2232. for increased speed, they are not as effective as this function.  In my
  2233. measurements I have found BufIn to be consistently four to five times
  2234. faster than BASIC's LINE INPUT routine when reading large (greater than
  2235. 50K) files.  With smaller files the improvement is less, but still
  2236. substantial.
  2237.  
  2238. DEFINT A-Z
  2239. DECLARE FUNCTION BufIn$ (FileName$, Done)
  2240.  
  2241. LINE INPUT "Enter a file name: ", FileName$
  2242.  
  2243. '---- Show how fast BufIn$ reads the file.
  2244. Start! = TIMER
  2245. DO
  2246.   This$ = BufIn$(FileName$, Done)
  2247.   IF Done THEN EXIT DO
  2248. LOOP
  2249. Done! = TIMER
  2250. PRINT "Buffered input: "; Done! - Start!
  2251.  
  2252.  
  2253. '---- Now show how long BASIC's LINE INPUT takes.
  2254. Start! = TIMER
  2255. OPEN FileName$ FOR INPUT AS #1
  2256. DO
  2257.   LINE INPUT #1, This$
  2258. LOOP UNTIL EOF(1)
  2259. Done! = TIMER
  2260. PRINT " BASIC's INPUT: "; Done! - Start!
  2261. CLOSE
  2262. END
  2263.  
  2264. FUNCTION BufIn$ (FileName$, Done) STATIC
  2265.  
  2266. IF Reading GOTO Process        'now reading, jump in
  2267.  
  2268. '----- initialization
  2269. Reading = -1                   'not reading so start now
  2270. Done = 0                       'clear Done just in case
  2271. CR$ = CHR$(13)                 'define for speed later
  2272.  
  2273. FileNum = FREEFILE             'open the file
  2274. OPEN FileName$ FOR BINARY AS #FileNum
  2275.  
  2276. Remaining& = LOF(FileNum)      'byte count to be read
  2277. IF Remaining& = 0 GOTO ExitFn  'empty or nonexistent file
  2278.  
  2279. BufSize = 4096                 'bytes to read each pass
  2280. Buffer$ = SPACE$(BufSize)      'assume BufSize bytes
  2281.  
  2282. DO                             'the main outer loop
  2283.   IF Remaining& < BufSize THEN 'read only what remains
  2284.     BufSize = Remaining&       'resize the buffer
  2285.     IF BufSize < 1 GOTO ExitFn 'possible only if EOF byte
  2286.     Buffer$ = SPACE$(BufSize)  'create the file buffer
  2287.   END IF
  2288.   GET #FileNum, , Buffer$      'read a block
  2289.  
  2290.   BufPos = 1                   'start at the beginning
  2291.   DO                                 'walk through buffer
  2292.     CR = INSTR(BufPos, Buffer$, CR$) 'look for a Return
  2293.     IF CR THEN                       'we found one
  2294.       SaveCR = CR                    'save where
  2295.       BufIn$ = MID$(Buffer$, BufPos, CR - BufPos)
  2296.       BufPos = CR + 2                'skip inevitable LF
  2297.       EXIT FUNCTION                  'all done for now
  2298.     ELSE                             'back up in the file
  2299.       '---- if at the end and no CHR$(13) was found
  2300.       '     return what remains in the string
  2301.       IF SEEK(FileNum) >= LOF(FileNum) THEN
  2302.         Output$ = MID$(Buffer$, SaveCR + 2)
  2303.         '---- trap a trailing EOF marker
  2304.         IF RIGHT$(Output$, 1) = CHR$(26) THEN
  2305.           Output$ = LEFT$(Output$, LEN(Output$) - 1)
  2306.         END IF
  2307.         BufIn$ = Output$             'assign the function
  2308.         GOTO ExitFn                  'and exit now
  2309.       END IF
  2310.       Slop = BufSize - SaveCR - 1    'calc buffer excess
  2311.       Remaining& = Remaining& + Slop 'calc file excess
  2312.       SEEK #FileNum, SEEK(FileNum) - Slop
  2313.     END IF
  2314.  
  2315. Process:
  2316.    LOOP WHILE CR               'while more in buffer
  2317.    Remaining& = Remaining& - BufSize
  2318.  
  2319. LOOP WHILE Remaining&          'while more in the file
  2320.  
  2321. ExitFn:
  2322.   Reading = 0                  'we're not reading anymore
  2323.   Done = -1                    'show that we're all done
  2324.   CLOSE #FileNum               'final clean-up
  2325.  
  2326. END FUNCTION
  2327.  
  2328. As you can see, the BufIn function opens the file, reads each line of text,
  2329. and then closes the file and sets a flags when it has exhausted the text. 
  2330. Even though this example show BufIn being invoked in a DO loop, it can be
  2331. used in any situation where LINE INPUT would normally be used.  As long as
  2332. you declare the function, it may be added to programs of your own and used
  2333. when sequential line-oriented data must be read as quickly as possible.
  2334.      I don't think each statement in the BufIn function warrants a complete
  2335. explanation, but some of the less obvious aspects do.  BufIn operates by
  2336. reading the file in 4K blocks in an outer loop, and each block is then
  2337. examined for a CHR$(13) line terminator in an inner loop that uses INSTR. 
  2338. INSTR happens to be extremely fast, and it is ideal when used this way to
  2339. search a string for a single character.
  2340.      The only real complication is when a portion of a string is in the
  2341. buffer, because that requires seeking backwards in the file to the start of
  2342. the string.  Other, less important complications that also must be handled
  2343. arise from the presence of a CHR$(26) EOF marker, and a final string that
  2344. has no terminating carriage return.
  2345.      I have made every effort to make this function as bullet-proof as
  2346. possible; however, it is mandatory that every carriage return in the file
  2347. be followed by a corresponding line feed.  Some word processors eliminate
  2348. the line feed to indicate a "soft return" at the end of a line, as opposed
  2349. to the "hard return" that signifies the end of a paragraph.  Most word
  2350. processor files use a non-standard format anyway, so that should not be
  2351. much of a problem.
  2352.      The last complete program I'll present here is called TEXTFIND.BAS,
  2353. and it searches a group of files for a specified string.  TEXTFIND is
  2354. particularly useful when you need to find a document, and cannot remember
  2355. its name.  If you can think of a snippet of text the file might contain,
  2356. TEXTFIND will identify which files contain that text, and then display it
  2357. in context.
  2358.  
  2359. '----- TEXTFIND.BAS
  2360.  
  2361. 'Copyright (c) 1991 by Ethan Winer
  2362.  
  2363. DEFINT A-Z
  2364.  
  2365. TYPE RegTypeX                   'used by CALL Interrupt
  2366.   AX    AS INTEGER
  2367.   BX    AS INTEGER
  2368.   CX    AS INTEGER
  2369.   DX    AS INTEGER
  2370.   BP    AS INTEGER
  2371.   SI    AS INTEGER
  2372.   DI    AS INTEGER
  2373.   Flags AS INTEGER
  2374.   DS    AS INTEGER
  2375.   ES    AS INTEGER
  2376. END TYPE
  2377. DIM Registers AS RegTypeX       'holds the CPU registers
  2378.  
  2379. TYPE DTA                        'used by DOS services
  2380.   Reserved  AS STRING * 21      'reserved for use by DOS
  2381.   Attribute AS STRING * 1       'the file's attribute
  2382.   FileTime  AS STRING * 2       'the file's time
  2383.   FileDate  AS STRING * 2       'the file's date
  2384.   FileSize  AS LONG             'the file's size
  2385.   FileName  AS STRING * 13      'the file's name
  2386. END TYPE
  2387. DIM DTAData AS DTA
  2388.  
  2389. DECLARE SUB InterruptX (IntNumber, InRegs AS RegTypeX, OutRegs AS RegTypeX)
  2390.  
  2391. CONST MaxFiles% = 1000
  2392. CONST BufMax% = 4096
  2393.  
  2394. REDIM Array$(1 TO MaxFiles%)    'holds the file names
  2395. Zero$ = CHR$(0)                 'do this once for speed
  2396.  
  2397. '----- This function returns the larger of two integers.
  2398. DEF FNMax% (Value1, Value2)
  2399.   FNMax% = Value1
  2400.   IF Value2 > Value1 THEN FNMax% = Value2
  2401. END DEF
  2402.  
  2403. '----- This function loads a group of file names.
  2404. DEF FNLoadNames%
  2405.  
  2406.   STATIC Count
  2407.  
  2408.   '---- define a new Data Transfer Area for DOS
  2409.   Registers.DX = VARPTR(DTAData)
  2410.   Registers.DS = VARSEG(DTAData)
  2411.   Registers.AX = &H1A00
  2412.   CALL InterruptX(&H21, Registers, Registers)
  2413.  
  2414.   Count = 0                  'zero the file counter
  2415.   Spec$ = Spec$ + Zero$      'DOS needs an ASCIIZ string
  2416.   Registers.DX = SADD(Spec$) 'show where the spec is
  2417.   Registers.DS = SSEG(Spec$)    'use this with PDS
  2418.  'Registers.DS = VARSEG(Spec$)  'use this with QB
  2419.   Registers.CX = 39          'the attribute for any file
  2420.   Registers.AX = &H4E00      'find file name service
  2421.  
  2422.   '---- Read the file names that match the search specification.  The Flags
  2423.   '     registers indicates when no more matching files are found.  Copy
  2424.   '     each file name to the string array.  Service &H4F is used to
  2425.   '     continue the search started with service &H4E using the same file
  2426.   '     specification.
  2427.   DO
  2428.     CALL InterruptX(&H21, Registers, Registers)
  2429.     IF Registers.Flags AND 1 THEN EXIT DO
  2430.     Count = Count + 1
  2431.     Array$(Count) = DTAData.FileName
  2432.     Registers.AX = &H4F00
  2433.   LOOP WHILE Count < MaxFiles%
  2434.  
  2435.   FNLoadNames% = Count       'return the number of files
  2436.  
  2437. END DEF
  2438.  
  2439. '----- The main body of the program begins here.
  2440. PRINT "TEXTFIND Copyright (c) 1991, Ziff-Davis Press."
  2441. PRINT
  2442.  
  2443. '---- Get the file specification, or prompt for one if it wasn't given.
  2444. Spec$ = COMMAND$
  2445. IF LEN(Spec$) = 0 THEN
  2446.   PRINT "Enter a file specification: ";
  2447.   INPUT "", Spec$
  2448. END IF
  2449.  
  2450. '----- Ask for the search string to find.
  2451. PRINT "    Enter the text to find: ";
  2452. INPUT Find$
  2453. PRINT
  2454.  
  2455. Find$ = UCASE$(Find$)        'ignore capitalization
  2456. FindLength = LEN(Find$)      'see how long Find$ is
  2457. IF FindLength = 0 THEN END
  2458.  
  2459. Count = FNLoadNames%         'load the file names
  2460. IF Count = 0 THEN
  2461.   PRINT "No matching files"
  2462.   END
  2463. END IF
  2464.  
  2465. '----- Isolate the drive and path if given.
  2466. FOR X = LEN(Spec$) TO 1 STEP -1
  2467.   Char = ASC(MID$(Spec$, X))
  2468.   IF Char = 58 OR Char = 92 THEN   '":" or "\"
  2469.     Path$ = LEFT$(UCASE$(Spec$), X)
  2470.     EXIT FOR
  2471.   END IF
  2472. NEXT
  2473.  
  2474. FOR X = 1 TO Count           'for each matching file
  2475.   Array$(X) = LEFT$(Array$(X), INSTR(Array$(X), Zero$) - 1)
  2476.   PRINT "Reading "; Path$; Array$(X)
  2477.   OPEN Path$ + Array$(X) FOR BINARY AS #1
  2478.   Length& = LOF(1)           'get and save its length
  2479.   IF Length& < FindLength GOTO NextFile
  2480.  
  2481.   BufSize = BufMax%          'assume a 4K text buffer
  2482.   IF BufSize > Length& THEN BufSize = Length&
  2483.   Buffer$ = SPACE$(BufSize)  'create the file buffer
  2484.  
  2485.   LastSeek& = 1              'seed the SEEK location
  2486.   BaseAddr& = 1              'and the starting offset
  2487.   Bytes = 0                  'how many bytes to search
  2488.  
  2489.   DO                         'the file read loop
  2490.      BaseAddr& = BaseAddr& + Bytes 'track block start
  2491.      IF Length& - LastSeek& + 1 >= BufSize THEN
  2492.        Bytes = BufSize       'at least BufSize bytes left
  2493.      ELSE                    'get just what remains
  2494.        Bytes = Length& - LastSeek& + 1
  2495.        Buffer$ = SPACE$(Bytes) 'adjust the buffer size
  2496.      END IF
  2497.  
  2498.      SEEK #1, LastSeek&      'seek back in the file
  2499.      GET #1, , Buffer$       'read a chunk of the file
  2500.  
  2501.      Start = 1               'this is the INSTR loop for
  2502.      DO                      'searching within the buffer
  2503.        Found = INSTR(Start, UCASE$(Buffer$), Find$)
  2504.        IF Found THEN         'print it in context
  2505.          Start = Found + 1   'to resume using INSTR later
  2506.          PRINT               'add a blank line for clarity
  2507.          PRINT MID$(Buffer$, FNMax%(1, Found - 20), FindLength + 40)
  2508.          PRINT
  2509.  
  2510.          PRINT "Continue searching "; Array$(X);
  2511.          PRINT "? (Yes/No/Skip): ";
  2512.          WHILE INKEY$ <> "": WEND   'clear kbd buffer
  2513.          DO
  2514.            KeyHit$ = UCASE$(INKEY$) 'then get a response
  2515.          LOOP UNTIL KeyHit$ = "Y" OR KeyHit$ = "N" OR KeyHit$ = "S"
  2516.          PRINT KeyHit$              'echo the letter
  2517.          PRINT
  2518.  
  2519.          IF KeyHit$ = "N" THEN      '"No"
  2520.            END                      'end the program
  2521.          ELSEIF KeyHit$ = "S" THEN  '"Skip"
  2522.            GOTO NextFile            'go to the next file
  2523.          END IF
  2524.  
  2525.        END IF
  2526.                                     'search for multiple hits
  2527.      LOOP WHILE Found               'within the file buffer
  2528.  
  2529.      IF Bytes = BufSize THEN        'still more file to examine
  2530.        '---- Back up a bit in case Find$ is there but straddling the buffer
  2531.        '     boundary.  Then update the internal SEEK pointer.
  2532.        BaseAddr& = BaseAddr& - FindLength
  2533.        LastSeek& = BaseAddr& + Bytes
  2534.      END IF
  2535.  
  2536.   LOOP WHILE Bytes = BufSize AND BufSize = BufMax%
  2537.  
  2538. NextFile:
  2539.   CLOSE #1
  2540.   Buffer$ = ""               'clear the buffer for later
  2541.  
  2542. NEXT
  2543. END
  2544.  
  2545. TEXTFIND may be run either in the BASIC editor or compiled to an executable
  2546. file and then run.  If you are using QuickBASIC you will need either QB.QLB
  2547. or QB.LIB because the program relies on CALL Interrupt to interface with
  2548. DOS.  To start QB and load the QB.QLB library simply enter qb /l.  If you
  2549. are compiling the program, specify the QB.LIB file when it is linked: 
  2550.  
  2551.      link textfind , , nul , qb;
  2552.  
  2553. For BASIC 7 users the appropriate library names are QBX.QLB and QBX.LIB
  2554. respectively.  [And for VB/DOS the libraries are VBDOS.QLB and VBDOS.LIB.]
  2555.      When you run TEXTFIND you may either enter a file specification such
  2556. as *.BAS or LET*.TXT or the like as a command line argument, or enter
  2557. nothing and let the program prompt you.  In either case, you will then be
  2558. asked to enter the text string you're searching for.  TEXTFIND will search
  2559. through every file that matches the file specification, and display the
  2560. string in context if it is found.
  2561.      As written, TEXTFIND shows the 20 characters before and after the
  2562. string.  You may of course modify that to any reasonable number of
  2563. characters.  Simple change the 20 and 40 values in the corresponding PRINT
  2564. statement.  The first value is the number of characters on either side to
  2565. display, and the second must be twice that to accommodate the length of the
  2566. search string itself.  Note the use of FNMax% which ensures that the
  2567. program will not try to print characters before the start of the buffer. 
  2568. If the text were found at the very start of the file, attempting to print
  2569. the 20 characters that precede it will create an "Illegal function call"
  2570. error at the MID$ function.
  2571.      Each time the string is found and displayed you are offered the
  2572. opportunity to continue searching the same file, ending the program, or
  2573. skipping to the next file.
  2574.      Although CALL Interrupt will be discussed in depth in Chapter 12,
  2575. there are several aspects of the program's operation that require
  2576. elaboration here.  First, any program that uses the DOS Find First and Find
  2577. Next services to read a list of file names must establish a small block of
  2578. memory as a Disk Transfer Area (DTA).  The DTA holds pertinent information
  2579. about each file that is found, such as its date, time, size, and attribute. 
  2580. In this case, though, we are merely interested in each file's name.  DOS
  2581. service &H1A is used to assign the DTA to a TYPE variable that is designed
  2582. to facilitate extracting this information.  BASIC PDS [and VB/DOS] include
  2583. the DIR$ function which lets you read file names, but I have used CALL
  2584. Interrupt here so the program will also work with QuickBASIC.
  2585.      Second, DEF FN-style functions are used instead of formal functions
  2586. because they are smaller and slightly faster.  The FNLoadNames function is
  2587. responsible for loading all of the file names into the string array, and it
  2588. returns the number of files that were found.  After each call to DOS to
  2589. find the next matching name, the Carry flag is tested.  DOS often uses the
  2590. carry flag to indicate the success or failure of an operation, and in this
  2591. case it is set to True when there are no more files.
  2592.      Note how a CHR$(0) is appended to the file specification when calling
  2593. DOS, to indicate the end of the string.  Similarly, DOS returns each file
  2594. name terminated with a zero byte, and INSTR is used to find that byte. 
  2595. Then, only those characters to the left of the zero are kept using LEFT$.
  2596.      Third, the block of code that isolates the drive and path name if
  2597. given is needed because the DOS Find services return only a file name.  If
  2598. you enter D:\ANYDIR\*.* as a file specification, that is then passed to
  2599. DOS.  But DOS returns only the names it finds that match the specification. 
  2600. Therefore, the drive and path must be added to the beginning of each name,
  2601. to create a complete file name for the subsequent OPEN command.
  2602.      Finally, as with the BufIn function, the files are read in 4K (4096-
  2603. byte) blocks, except for the last block which of course may be smaller.  A
  2604. smaller block is also used when the file is less than 4K in length.  Within
  2605. each outer read loop, an inner loop is employed to search for the text, and
  2606. again INSTR is used because of its speed.  As written, TEXTFIND looks for
  2607. the specified string without regard to capitalization.  You can remove that
  2608. feature by eliminating the UCASE$ function in both the INSTR loop, and at
  2609. the point in the program where Find$ is capitalized.
  2610.  
  2611.  
  2612. MINIMIZING DISK USAGE
  2613.  
  2614. While improving your program's performance is certainly a desireable
  2615. pursuit, equally important is minimizing the amount of space needed to
  2616. store data.  Besides the obvious savings in disk space, the less data there
  2617. is, the faster it can be loaded and saved.  There are a number of simple
  2618. tricks you can use to reduce the size of your data files, and some types of
  2619. data lend themselves quite nicely to compaction techniques.
  2620.      Date information is particularly easy to reduce.  At the minimum, you
  2621. should remove the separating slashes or dashes--perhaps with a dedicated
  2622. function.  For example, you would convert "06-22-91" to "062291".  Even
  2623. better, however, is to convert each digit pair to an equivalent single
  2624. CHR$() byte, and also swap the order of the digits.  That is, the date
  2625. above would be packed to CHR$(91) + CHR$(6) + CHR$(22).  By placing the
  2626. year first followed by the month and then the day, dates may also be
  2627. compared.  Otherwise, a normal string comparison would show the date "01-
  2628. 01-91" as being less (earlier) than "12-31-90" even though it is in fact
  2629. greater (later).  A complementary function would then extract the ASCII
  2630. values into a date string suitable for display.  These are shown below.
  2631.  
  2632. DEFINT A-Z
  2633. DECLARE FUNCTION PackDate$ (D$)
  2634. DECLARE FUNCTION UnPackDate$ (D$)
  2635.  
  2636. D$ = "03-22-91"
  2637. Packed$ = PackDate$(D$)
  2638. UnPacked$ = UnPackDate$(Packed$)
  2639.  
  2640. PRINT D$
  2641. PRINT Packed$
  2642. PRINT UnPacked$
  2643. END
  2644.  
  2645. FUNCTION PackDate$ (D$) STATIC
  2646.   Year = VAL(RIGHT$(D$, 2))
  2647.   Month = VAL(LEFT$(D$, 2))
  2648.   Day = VAL(MID$(D$, 4, 2))
  2649.   PackDate$ = CHR$(Year) + CHR$(Month) + CHR$(Day)
  2650. END FUNCTION
  2651.  
  2652. FUNCTION UnPackDate$ (D$) STATIC
  2653.   Month$ = LTRIM$(STR$(ASC(MID$(D$, 2, 1))))
  2654.   Day$ = LTRIM$(STR$(ASC(RIGHT$(D$, 1))))
  2655.   Year$ = LTRIM$(STR$(ASC(LEFT$(D$, 1))))
  2656.   UnPackDate$ = RIGHT$("0" + Month$, 2) + "-" + RIGHT$("0" + Day$, 2) + _
  2657.     "-" + RIGHT$("0" + Year$, 2)
  2658. END FUNCTION
  2659.  
  2660. Because the compacted dates will likely contain a CHR$(26) byte which is
  2661. used by DOS and BASIC as an EOF marker, this method is useful only with
  2662. random access and binary data files.  But since it is usually large
  2663. database files that need the most help anyway, these functions are ideal.
  2664.      Another useful database compaction technique is to replace selected
  2665. strings with an equivalent integer or byte value.  The commercial database
  2666. program *DataEase* uses a very clever trick to implement multiple choice
  2667. fields.  It is not uncommon to have a string field that contains, say, an
  2668. income or expense category.  For example, most businesses are required to
  2669. indicate the purpose of each check that is written.  Instead of using a
  2670. string field and requiring the operator to type Entertainment, Payroll, or
  2671. whatever, a menu can be popped up showing a list of possible choices.
  2672.      Assuming there are no more than 256 possibilities, the choice number
  2673. that was entered can be stored on disk in a single byte.  You would use
  2674. something like FileType.Choice = CHR$(MenuChoice), where the Choice portion
  2675. of the file type was defined as STRING * 1.  Then to extract the choice
  2676. after a record was read you would use MenuChoice = ASC(FileType.Choice).
  2677.      Some database programs support Memo Fields, whereby the user can enter
  2678. a varying amount of memo information.  Since database files almost always
  2679. use a fixed length for each record, this presents a programming dilemma:
  2680. How much space do you set aside for the memo field?  If you set aside too
  2681. little, the user won't be very pleased.  But setting aside enough to
  2682. accommodate the longest possible string is very wasteful of disk space.
  2683.      One good solution is to store a long integer pointer in each record,
  2684. and keep the memos themselves in a separate file.  A long integer requires
  2685. only four bytes of storage, yet it can hold a seek location for memo data
  2686. kept in a separate file whose size can be greater than 2000 MB!  As each
  2687. new memo is entered, the current length [derived using LOF] of the memo
  2688. file is written in the current record of the data file.  The memo string is
  2689. then appended to the memo file.  When you want to retrieve the memo, simply
  2690. seek to the long integer offset held in the main data record and use LINE
  2691. INPUT to read the string from the memo file.
  2692.      The only real complication with this method is when a memo field must
  2693. be edited.  There's no reasonable way to lengthen or shorten data in the
  2694. middle of a file, and no reasonable program would even try.  Instead, you
  2695. would simply overwrite the existing data with special values--perhaps with
  2696. CHR$(255) bytes--and then append the new memo to the end of the file. 
  2697. Periodically you would have to run a utility program that copied only the
  2698. valid memo fields to a new file, and then delete the old file.  Be aware
  2699. that you will also have to update the long integer pointers in the main
  2700. data file, to reflect the new offsets of their corresponding memo fields.
  2701.      The last data size reduction technique is probably the simplest of
  2702. all, and that is to use the appropriate type of data and file access
  2703. method.  If you can get by with a single precision variable, don't use a
  2704. double precision.  And if the range of integer values is sufficient, use
  2705. those.  Many programmers automatically use single precision variables
  2706. without even thinking about it, when a smaller data type would suffice.
  2707.      Finally, avoid using sequential files to store numeric data.  As I
  2708. already pointed out, an integer can be stored in a binary file in only two
  2709. bytes--no matter what its value--compared to as many as eight bytes needed
  2710. to store the equivalent digits, possible minus sign, and a terminating
  2711. carriage return and line feed.  Be creative, and don't be afraid to invent
  2712. a method that is suited to your particular application.  The Lotus format
  2713. is a good one for many other applications, whereby a size and type code
  2714. precedes each piece of information.  If your needs are modest you can
  2715. probably get away with a single byte as a type code, further reducing the
  2716. amount of storage that is needed.
  2717.  
  2718.  
  2719. AVOIDING BASIC'S LIMITATIONS
  2720.  
  2721. So far I have focused on improving what BASIC already does.  I showed
  2722. techniques for speeding up file accesses, and reducing the size of your
  2723. data.  I even showed how to overcome BASIC's unwillingness to directly
  2724. write binary data larger than a single variable.  But there are other BASIC
  2725. limitations that can be overcome as well.
  2726.      One important limitation is that BASIC lets you run only .EXE files
  2727. with the RUN statement.  If you need to execute a .COM program or a batch
  2728. file, BASIC will not let you.  However you can trick DOS into believing a
  2729. .COM program or batch file's name was entered at the DOS prompt.  The
  2730. StuffBuffer subprogram shown below inserts a string of up to 15 characters
  2731. directly into the keyboard buffer.  It works by poking each character one
  2732. by one into the buffer address in low memory.  Thus, when your program ends
  2733. the characters are there as if someone had typed them manually.
  2734.  
  2735. DEFINT A-Z
  2736. DECLARE SUB StuffBuffer (Cmd$)
  2737.  
  2738. SUB StuffBuffer (Cmd$) STATIC
  2739.  
  2740.   '----- Limit the string to 14 characters plus Enter and save the length.
  2741.   Work$ = LEFT$(Cmd$, 14) + CHR$(13)
  2742.   Length = LEN(Work$)
  2743.  
  2744.   '----- Set the segment for poking, define the buffer head and tail, and
  2745.   '      then poke each character.
  2746.   DEF SEG = 0
  2747.   POKE 1050, 30
  2748.   POKE 1052, 30 + Length * 2
  2749.   FOR X = 1 TO Length
  2750.     POKE 1052 + X * 2, ASC(MID$(Work$, X))
  2751.   NEXT
  2752.  
  2753. END SUB
  2754.  
  2755. To run a .COM program or batch file simply call StuffBuffer and end the
  2756. program:  
  2757.  
  2758.      CALL StuffBuffer("PROGRAM"): END
  2759.  
  2760. A terminating carriage return is added to the command, to include a final
  2761. Enter keypress.  Because the keyboard buffer holds only 15 characters, you
  2762. cannot specify long path names when using StuffBuffer.  However, you can
  2763. easily open and write a short batch file with the complete path and file
  2764. name, and run the batch file instead.
  2765.      Notice that this technique will not work if the original BASIC program
  2766. itself has been run from a batch file, because that batch file gains
  2767. control when the program ends.  Also, when creating and running a batch
  2768. file that will be run by StuffBuffer, it is imperative that the last line
  2769. *not* have a terminating carriage return.  The short example below shows
  2770. the correct way to create and run a batch file for use with StuffBuffer.
  2771.  
  2772.  
  2773. OPEN "MYBAT.BAT" FOR OUTPUT AS #1
  2774.   PRINT #1, "cd \somedir"
  2775.   PRINT #1, "someprog";
  2776. CLOSE
  2777. CALL StuffBuffer("MYBAT")
  2778. END
  2779.  
  2780.  
  2781. You can also have the batch file re-run the BASIC program by entering its
  2782. name as the last line in the batch file.  In that case you would include
  2783. the semicolon at the end of that line, instead of the line that runs the
  2784. program.  Note that StuffBuffer is an ideal replacement for BASIC's SHELL
  2785. command, because with SHELL your BASIC program remains in memory while the
  2786. subsequent program is run.  Using StuffBuffer with a batch file removes the
  2787. BASIC program entirely, thus freeing up all available system memory for the
  2788. program being run.
  2789.      Understand that StuffBuffer cannot be used to activate a TSR or other
  2790. program that monitors keyboard interrupt 9.  This limitation also extends
  2791. to the special key sequences that enable the Turbo mode on some PC
  2792. compatibles, and simulating Ctrl-Esc to activate the DOS compatibility box
  2793. of OS/2.  Programs that look for these special keys insert themselves into
  2794. the keyboard chain *before* the keyboard buffer, and act on them before the
  2795. BIOS has the chance to store them in the buffer.
  2796.      Another BASIC limitation is that only 15 files may be open at one
  2797. time.  In truth, this is really a DOS limitation, and indeed, the fix
  2798. requires a DOS interrupt service.  It is also possible to reduce the number
  2799. of files open at once by combining data.  For example, the BASIC PDS ISAM
  2800. file manager uses this technique to store both the data and its indexes all
  2801. in the same file.  But doing that requires more complication than many
  2802. programmers are willing to put up with.
  2803.      The program below shows how to increase the number of files that DOS
  2804. will let you open.  Be aware that the DOS service that performs this magic
  2805. requires at least version 3.3, and this program tests for that.
  2806.  
  2807. DEFINT A-Z
  2808. DECLARE SUB Interrupt (IntNum, InRegs AS ANY, OutRegs AS ANY)
  2809. DECLARE SUB MoreFiles (NumFiles)
  2810. DECLARE FUNCTION DOSVer% ()
  2811.  
  2812. TYPE RegType
  2813.   AX    AS INTEGER
  2814.   BX    AS INTEGER
  2815.   CX    AS INTEGER
  2816.   DX    AS INTEGER
  2817.   BP    AS INTEGER
  2818.   SI    AS INTEGER
  2819.   DI    AS INTEGER
  2820.   Flags AS INTEGER
  2821. END TYPE
  2822. DIM SHARED InRegs AS RegType, OutRegs AS RegType
  2823.  
  2824. ComSpec$ = ENVIRON$("COMSPEC")
  2825. BootDrive$ = LEFT$(ComSpec$, 2)
  2826. OPEN BootDrive$ + "\CONFIG.SYS" FOR INPUT AS #1
  2827.   DO WHILE NOT EOF(1)
  2828.     LINE INPUT #1, Work$
  2829.     Work$ = UCASE$(Work$)
  2830.     IF LEFT$(Work$, 6) = "FILES=" THEN
  2831.       FilesVal = VAL(MID$(Work$, 7))
  2832.       EXIT DO
  2833.     END IF
  2834.   LOOP
  2835. CLOSE
  2836.  
  2837. INPUT "How many files? ", NumFiles
  2838. NumFiles = NumFiles + 5
  2839. IF NumFiles > FilesVal THEN
  2840.   PRINT "Increase the FILES= setting in CONFIG.SYS"
  2841.   END
  2842. END IF
  2843.  
  2844. IF DOSVer% >= 330 THEN
  2845.   CALL MoreFiles(NumFiles)
  2846. ELSE
  2847.   PRINT "Sorry, DOS 3.3 or later is required."
  2848.   END
  2849. END IF
  2850.  
  2851. FOR X = 1 TO NumFiles
  2852.   OPEN "FTEST" + LTRIM$(STR$(X)) FOR RANDOM AS #X
  2853. NEXT
  2854. CLOSE
  2855. KILL "FTEST*."
  2856. END
  2857.  
  2858. FUNCTION DOSVer% STATIC
  2859.   InRegs.AX = &H3000
  2860.   CALL Interrupt(&H21, InRegs, OutRegs)
  2861.   Major = OutRegs.AX AND &HFF
  2862.   Minor = OutRegs.AX \ &H100
  2863.   DOSVer% = Minor + 100 * Major
  2864. END FUNCTION
  2865.  
  2866. SUB MoreFiles (NumFiles) STATIC
  2867.   InRegs.AX = &H6700
  2868.   InRegs.BX = NumFiles
  2869.   CALL Interrupt(&H21, InRegs, OutRegs)
  2870. END SUB
  2871.  
  2872. As with the TEXTFIND program, this also uses CALL Interrupt and therefore
  2873. requires QB.LIB and QB.QLB to compile or run in the QuickBASIC environment
  2874. respectively.  Even though DOS allows you to increase the number of files
  2875. past the default 15, an appropriate FILES= statement must also be added to
  2876. the PC's CONFIG.SYS file.  In fact, the FILES= value must be five greater
  2877. than the desired number of files, because DOS reserves the first five for
  2878. itself.  The reserved files [devices] are PRN, AUX, STDIN, STDOUT, and
  2879. STDERR.  PRN is of course the printer connected to LPT1, AUX is the first
  2880. COM port, and the remaining devices are all part of the CON console device.
  2881.      In order to find the CONFIG.SYS file this program uses the ENVIRON$
  2882. function to retrieve the current COMSPEC= setting.  Unless someone has
  2883. changed it on purpose, the COMSPEC environment variable holds the drive and
  2884. path from which the PC was booted, and the file name "COMMAND.COM".  Then
  2885. each line in CONFIG.SYS is examined for the string "FILES=", to ensure that
  2886. enough file entries were specified.  This program makes only a minimal
  2887. attempt to identify the "FILES=" string, so if there are extra spaces such
  2888. as "FILES = 30" the test will fail.
  2889.      Next the DOS version is tested to ensure that it is version 3.3 or
  2890. later.  The DOSVer function is designed to return the DOS version as an
  2891. integer value 100 times higher than the actual version number.  That is,
  2892. DOS 2.14 is returned as 214, and DOS 3.30 is instead 330.  This eliminates
  2893. the floating point math required to return a value such as 2.14 or 3.3,
  2894. resulting in less code and faster operation.
  2895.      Assuming the FILES= setting is sufficiently high and the DOS version
  2896. is at least 3.30, the program creates and then deletes the specified number
  2897. of files just to show it worked.  You should be aware that the BASIC editor
  2898. must also open files when it saves your program.  I mention this because it
  2899. is possible to be experimenting with a program such as this one, and not be
  2900. able to save your work because the maximum allowable number of files are
  2901. already open.  In that case BASIC issues a "Too many files" error message,
  2902. and refuses to let you save.  The solution is to press F6 to go to the
  2903. Immediate window, and then type CLOSE.
  2904.      A similar situation happens when you try to shell to DOS from the
  2905. BASIC editor, because shelling requires BASIC to open COMMAND.COM.  But an
  2906. unsuccessful shell results in an "Illegal function call" error.  That
  2907. message is particularly exasperating when BASIC's SHELL fails, because the
  2908. failure is usually caused by insufficient memory or because COMMAND.COM
  2909. cannot be located.  Why Microsoft chose to return "Illegal function call"
  2910. rather than "Out of memory", "File not found", or "Too many files" is
  2911. anyone's guess.
  2912.      Another important BASIC limitation that can be overcome only with
  2913. clever trickery is its inability to "map" multiple variables to the same
  2914. memory address.  This is an important feature of the C language, and it has
  2915. some important applications.  For example, if you are frequently accessing
  2916. a group of characters in the middle of a string, you must use MID$ each
  2917. time you assign or retrieve them.  Unfortunately, MID$ is very slow because
  2918. it always extracts a copy of the specified characters, even if you are
  2919. merely printing them.  If only BASIC would let you create a new string that
  2920. always referred to that group of characters in the first string, the access
  2921. speed could be greatly improved.
  2922.      The FIELD statement lets you do exactly this, and each time a new
  2923. FIELD statement is encountered the same area of memory is referred to.  The
  2924. short example below shows the tremendous speed improvement possible only
  2925. when two variables can occupy the same address.  An additional trick used
  2926. here is to open the DOS reserved "\DEV\NUL" device.  This eliminates any
  2927. disk access, and avoids also having to create an empty file just to
  2928. implement the FIELD statement.
  2929.  
  2930. DEFINT A-Z
  2931.  
  2932. OPEN "\DEV\NUL" FOR RANDOM AS #1 LEN = 30
  2933. FIELD #1, 10 AS First$, 10 AS Middle$, 10 AS Last$
  2934. FIELD #1, 30 AS Entire$
  2935. LSET Entire$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234"
  2936. Start! = TIMER
  2937. FOR X = 1 TO 20000
  2938.   Temp = ASC(Middle$)
  2939. NEXT
  2940. Done! = TIMER
  2941. PRINT USING "##.### seconds for FIELD"; Done! - Start!
  2942. CLOSE
  2943.  
  2944. Entire$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234"
  2945. Start! = TIMER
  2946. FOR X = 1 TO 20000
  2947.   Temp = ASC(MID$(Entire$, 10, 10))
  2948. NEXT
  2949. Done! = TIMER
  2950. PRINT USING "##.### seconds for MID$"; Done! - Start!
  2951.  
  2952. As you can see, accessing Middle$ as defined in the FIELD statement is more
  2953. than three times faster than accessing the middle portion of Entire$ using
  2954. MID$.  There are no doubt other situations where it is useful to treat the
  2955. same area of memory as different variables, perhaps to provide different
  2956. views [such as numeric and string] of the same data.  We can only hope that
  2957. Microsoft will see fit to add this important capability to a future version
  2958. of BASIC.  [PowerBASIC offers this feature via the UNION command.]
  2959.      The NUL device has other important applications in conjunction with
  2960. FIELD.  One common programming problem that comes up frequently is being
  2961. able to format numbers to a controlled number of decimal places.  Although
  2962. BASIC's PRINT USING will format a number and write it to the screen, there
  2963. is no way to actually access the formatted value.  It is possible to have
  2964. PRINT USING write the value on the screen--perhaps in the upper left corner
  2965. with a color setting of black on black--and then read it character by
  2966. character with SCREEN.  But that method is clunky at best, and also very
  2967. slow.
  2968.      The short program below uses PRINT USING # to write to a fielded
  2969. buffer, and then LINE INPUT # to read the number back from the buffer.
  2970.  
  2971.  
  2972. Value# = 123.45678#
  2973.  
  2974. OPEN "\DEV\NUL" FOR RANDOM AS #1 LEN = 15
  2975. FIELD #1, 15 AS Format$
  2976. PRINT #1, USING "####.##"; Value#
  2977. LINE INPUT #1, Fmt$
  2978.  
  2979. PRINT "    Value:"; Value#
  2980. PRINT "Formatted:"; Fmt$
  2981.  
  2982.  
  2983. Notice that the field buffer must be long enough to receive the entire
  2984. formatted string, including the carriage return and line feed that BASIC
  2985. sends as part of the PRINT # statement.  This technique opens up many
  2986. exciting possibilities, especially when used in conjunction with PRINT #
  2987. USING's other extensive formatting options.
  2988.      [PDS includes the FORMAT$ function externally in Quick and regular
  2989. link libraries, and VB/DOS goes a step further by adding FORMAT$ to the
  2990. language.  But FORMAT$ offers only a subset of what PRINT USING can do.]
  2991.  
  2992.  
  2993. ADVANCED DEVICE TECHNIQUES
  2994. ==========================
  2995.  
  2996. As many tricks as there are for reading and writing files, there are just
  2997. as many for accessing devices.  Many devices such as printers and modems
  2998. are so much slower than BASIC that the techniques for sending large amounts
  2999. of data in one operation are not needed or useful.  But these devices offer
  3000. a whole new set of problems that just beg for clever programming solutions. 
  3001. With that in mind, let's continue this tour and examine some of the less
  3002. obvious aspects of BASIC's device handling capabilities.
  3003.  
  3004.  
  3005. THE PRINTER DEVICE
  3006.  
  3007. All modern printers accept special control codes to enable and disable
  3008. underlining, boldfacing, italics, and sometimes even font changes.  Many
  3009. printers honor the standard Epson/IBM control codes, and some recognize
  3010. additional codes to control unique features available only with that brand
  3011. or model.  However, it is possible to print underline and boldface text
  3012. with most printers, without regard to the particular model.  The examples
  3013. shown below require that you open the printer as a device using "LPT1:BIN". 
  3014. If you are using LPT2, of course, then you will open "LPT2:BIN" instead. 
  3015. As I mentioned earlier, the BIN option tells BASIC not to interfere with
  3016. any control codes you send, and also not to add automatic line wrapping.
  3017.      Most programmers assume that every carriage return is always
  3018. accompanied by a corresponding line feed, and indeed, that is almost always
  3019. the case.  Even if you print a CHR$(13) carriage return followed by a
  3020. semicolon, BASIC steps in and appends a line feed for you.  But these are
  3021. separate characters, and each can be used separately to control a printer. 
  3022. The example below prints a short string and a carriage return *without* a
  3023. line feed, and then prints a series of underlines beneath the string.
  3024.  
  3025.  
  3026. OPEN "LPT1:BIN" FOR OUTPUT AS #1
  3027. PRINT #1, "BASIC Techniques and Utilities"; CHR$(13);
  3028. PRINT #1, "      __________"
  3029. CLOSE
  3030.  
  3031.  
  3032. Similarly, you can also simulate boldfacing by printing the same string at
  3033. the same place on the paper two or three times.  While this won't work with
  3034. a laser printer, it is very effective on dot matrix printers.  Of course,
  3035. if you do know the correct control codes for the printer, then those can be
  3036. sent directly.  Be sure, however, to always include a trailing semicolon as
  3037. part of the print statement, to avoid also sending an unwanted return and
  3038. line feed.  For example, to advance a printer to the start of the next page
  3039. you would use either PRINT #1, CHR$(12); or LPRINT CHR$(12);.  In this
  3040. case, a normal LPRINT will work because you are not sending a CHR$(13) or
  3041. CHR$(10).
  3042.      Most printers also accept a CHR$(8) to indicate a backspace, which may
  3043. simplify underlining in some cases.  That is, instead of printing a
  3044. CHR$(13) to go the start of the line, you would print the string, and
  3045. simply back up the print head the appropriate number of columns.  BASIC's
  3046. STRING$ function is ideal for this, using LPRINT STRING$(Count, 8); to send
  3047. Count backspace characters to the printer.
  3048.      You can also send a complete font file to a printer with the CopyFile
  3049. program shown earlier.  Simply give the font file's name as the source, and
  3050. the string "LPT1:BIN" as the destination.
  3051.  
  3052.  
  3053. THE SCREEN DEVICE
  3054.  
  3055. As with printers, there are a number of ways to manipulate the display
  3056. screen by printing special control characters.  Where a CHR$(12) can be
  3057. used to advance the printer to the top of the next page, this same
  3058. character will clear the screen and place the cursor at the upper left
  3059. corner.  Printing a CHR$(11) will home the cursor only, and printing a
  3060. CHR$(7) beeps the speaker.
  3061.      Another useful screen control character is CHR$(9), which advances to
  3062. the next tab stop.  Tab stops are located at every eighth column, with the
  3063. first at column 9, the second at column 17, and so forth.  As with a
  3064. printer that has not been opened using the BIN option, printing either a
  3065. CHR$(10) or a CHR$(13)--even with a semicolon--always sends the cursor to
  3066. the beginning of the next line.  There is unfortunately no way to separate
  3067. the actions of a carriage return and line feed.
  3068.      The last four control characters that are useful with the screen are
  3069. CHR$(28), CHR$(29), CHR$(30), and CHR$(31).  These move the cursor forward,
  3070. backward, up a line (if possible) and down a line (if possible).  Although
  3071. LOCATE can be used to move the cursor, these commands allow you to do it
  3072. relative to the current location.  To do the same with LOCATE would require
  3073. code like this: IF POS(0) > 1 THEN LOCATE , POS(0) - 1.  Obviously, the
  3074. control characters will result in less generated code, because they avoid
  3075. the IF test and repeated calls to BASIC's POS(0) function.
  3076.      BASIC PDS includes a series of stub files named TSCNIOxx.OBJ that
  3077. eliminate support for all graphics statements, and also ignore the control
  3078. characters listed above.  Because each character must be tested
  3079. individually by BASIC as it looks for these control codes, using these stub
  3080. files will increase the speed of your program's display output.
  3081.      All versions of Microsoft BASIC have always included the WIDTH
  3082. statement for controlling the number of columns on the screen.  With the
  3083. introduction of QuickBASIC 3.0, SCREEN was expanded to also allow setting
  3084. the number of rows on EGA and VGA monitors.  The statement WIDTH , 43 puts
  3085. the screen into the 43-line text mode, and may be used with an EGA or VGA
  3086. display.  WIDTH , 50 is valid for VGA monitors only, and as you can
  3087. imagine, it switches the display to the 50-line text mode.
  3088.      In many cases it is necessary to know if the display screen is color
  3089. or monochrome, and also if it is capable of supporting the EGA or VGA
  3090. graphics modes.  The simplest way to detect a color monitor is to look at
  3091. the display adapter's port address in low memory.  The short code fragment
  3092. below shows how this is done.
  3093.  
  3094.  
  3095. DEF SEG = 0
  3096. IF PEEK(&H463) = &HB4 THEN
  3097.   '---- it's a monochrome monitor
  3098. ELSE
  3099.   '---- it's a color monitor
  3100. END IF
  3101.  
  3102.  
  3103. This information is important if you plan to BLOAD a screen image directly
  3104. into video memory.  If the display adapter is reported as monochrome, then
  3105. you would use DEF SEG to set the segment to &HB000.  A color monitor in
  3106. text mode instead uses segment &HB800.  Knowing if a monitor has color
  3107. capabilities also helps you to choose appropriate color values, and tells
  3108. you if it can support graphics.  But you will need to know which video
  3109. modes the display adapter is capable of.
  3110.      Detecting an EGA or VGA is more complex than merely distinguishing
  3111. between monochrome and color, because it requires calling a video interrupt
  3112. service routine located on the display adapter card.  A Hercules monitor is
  3113. also difficult to detect, because that requires a timing loop to see if the
  3114. Hercules video status port changes.  All of this is taken into account in
  3115. the example and function that follows.
  3116.  
  3117. DEFINT A-Z
  3118.  
  3119. DECLARE SUB Interrupt (IntNum, InRegs AS ANY, OutRegs AS ANY)
  3120. DECLARE FUNCTION Monitor% (Segment)
  3121.  
  3122. TYPE RegType
  3123.   AX    AS INTEGER
  3124.   BX    AS INTEGER
  3125.   CX    AS INTEGER
  3126.   DX    AS INTEGER
  3127.   BP    AS INTEGER
  3128.   SI    AS INTEGER
  3129.   DI    AS INTEGER
  3130.   Flags AS INTEGER
  3131. END TYPE
  3132. DIM SHARED InRegs AS RegType, OutRegs AS RegType
  3133.  
  3134. SELECT CASE Monitor%(Segment)
  3135.   CASE 1
  3136.     PRINT "Monochrome";
  3137.   CASE 2
  3138.     PRINT "Hercules";
  3139.   CASE 3
  3140.     PRINT "CGA";
  3141.   CASE 4
  3142.     PRINT "EGA";
  3143.   CASE 5
  3144.     PRINT "VGA";
  3145.   CASE ELSE
  3146.     PRINT "Unknown";
  3147. END SELECT
  3148. PRINT " monitor at segment &H"; HEX$(Segment)
  3149.  
  3150. FUNCTION Monitor% (Segment) STATIC
  3151.  
  3152.   DEF SEG = 0                 'first see if it's color or mono
  3153.   Segment = &HB800            'assume color
  3154.  
  3155.   IF PEEK(&H463) = &HB4 THEN  'it's monochrome
  3156.  
  3157.     Segment = &HB000          'assign the monochrome segment
  3158.     Status = INP(&H3BA)       'get the current video status
  3159.     FOR X = 1 TO 30000        'test for a Hercules 30000 times
  3160.       IF INP(&H3BA) <> Status THEN
  3161.         Monitor% = 2          'the port changed, it's a Herc
  3162.         EXIT FUNCTION         'all done
  3163.       END IF
  3164.     NEXT
  3165.     Monitor% = 1              'it's a plain monochrome
  3166.  
  3167.   ELSE                        'it's some sort of color monitor
  3168.  
  3169.     InRegs.AX = &H1A00        'first test for VGA
  3170.     CALL Interrupt(&H10, InRegs, OutRegs)
  3171.     IF (OutRegs.AX AND &HFF) = &H1A THEN
  3172.       Monitor% = 5            'it's a VGA
  3173.       EXIT FUNCTION           'all done
  3174.     END IF
  3175.  
  3176.     InRegs.AX = &H1200        'now test for EGA
  3177.     InRegs.BX = &H10
  3178.     CALL Interrupt(&H10, InRegs, OutRegs)
  3179.     IF (OutRegs.BX AND &HFF) = &H10 THEN
  3180.       Monitor% = 3            'if BL is still &H10 it's a CGA
  3181.     ELSE
  3182.       Monitor% = 4            'otherwise it's an EGA
  3183.     END IF
  3184.  
  3185.   END IF
  3186.  
  3187. END FUNCTION
  3188.  
  3189. The Monitor function returns both the type of monitor that is active, as
  3190. well as the video segment that is used when displaying text.  EGA and VGA
  3191. displays use segment &HA000 for graphics, which is a different issue
  3192. altogether.  Monitor is particularly valuable when you need to know what
  3193. SCREEN modes a given display adapter can support.  The *only* alternative
  3194. is to use ON ERROR and try each possible SCREEN value in a loop starting
  3195. from the highest resolution.  When SCREEN finally reaches a low enough
  3196. value to succeed, then you know what modes are legal.  Since BASIC knows
  3197. the type of monitor installed, it seems inconceivable to me that this
  3198. information is not made available to your program.  [PowerBASIC uses an
  3199. internal variable to hold the display type, and that variable is available
  3200. to the programmer.]
  3201.      Notice that the Registers TYPE variable is dimensioned in the example
  3202. portion of this program, and not in the Monitor function itself.  Each time
  3203. a TYPE or fixed-length string variable is dimensioned in a STATIC
  3204. subprogram or function, new memory is allocated permanently to hold it.  In
  3205. this short program the Registers TYPE variable is used only once.  But in a
  3206. real program that incorporates many of the routines from this chapter,
  3207. memory can be saved by using DIM SHARED in the main program.  Then, each
  3208. subroutine can use the same variable for its own use.
  3209.      Once you know the type of monitor, you will also know what color
  3210. combinations are valid and readable.  A color monitor can of course use any
  3211. combination of foreground and background colors, but a monochrome is
  3212. limited to the choices shown in Table 6-3.  Combinations not listed will
  3213. result in text that is unreadable on a many monochrome monitors.
  3214.  
  3215.  
  3216. Color as Displayed                 COLOR Values
  3217. ────────────────────────────────   ────────────
  3218. White on Black                     COLOR 7, 0  
  3219. Bright White on Black              COLOR 15, 0
  3220. Black on White                     COLOR 0, 7
  3221. White Underlined on Black          COLOR 1, 0
  3222. Bright White Underlined on Black   COLOR 9, 0
  3223.  
  3224. Table 6-3: Valid Color Combinations For Use With a Monochrome Monitor.
  3225.  
  3226.  
  3227. It is important to point out that some computers employ a CGA display
  3228. adapter connected to a monochrome monitor.  For example, the original
  3229. Compaq portable PC used this arrangement.  Many laptop computers also have
  3230. a monochrome display connected to a CGA, EGA, or VGA adapter.  Since it is
  3231. impossible for a program to look beyond the adapter hardware through to the
  3232. monitor itself, you will need to provide a way for users with that kind of
  3233. hardware to alert your program.
  3234.      The BASIC editor recognizes a /b command line switch to indicate black
  3235. and white operation, and I suggest that you do something similar.  Indeed,
  3236. many commercial programs offer a way for the user to indicate that color
  3237. operation is not available or desired.
  3238.      The last video-related issue I want to cover is saving and loading
  3239. text and graphics images.  As you probably know, the memory organization of
  3240. a display adapter when it is in one of the graphics modes is very different
  3241. than when it is in text mode.  In the text mode, each character and its
  3242. corresponding color byte are stored in contiguous memory locations in the
  3243. appropriate video segment.  All of the color text modes store the
  3244. characters and their colors at segment &HB800, while monochrome displays
  3245. use segment &HB000.
  3246.      The character in the upper left corner of the screen is at address 0
  3247. in the video segment, and its corresponding color is at address 1.  The
  3248. character currently at screen location (1, 2) is stored at address 2, and
  3249. its color is at address 3, and so forth.  The brief program fragment below
  3250. illustrates this visually by using POKE to write a string of characters and
  3251. colors directly to display memory.
  3252.  
  3253. DEFINT A-Z
  3254.  
  3255. CLS
  3256. LOCATE 20
  3257. PRINT "Keep pressing a key to continue"
  3258.  
  3259. DEF SEG = 0
  3260. IF PEEK(&H463) = &HB4 THEN
  3261.   DEF SEG = &HB000
  3262. ELSE
  3263.   DEF SEG = &HB800
  3264. END IF
  3265.  
  3266. Test$ = "Hello!"
  3267. Colr = 9                        'bright blue or underlined
  3268.  
  3269. FOR X = 1 TO LEN(Test$)         'walk through the string
  3270.   Char = ASC(MID$(Test$, X, 1)) 'get this character
  3271.   POKE Address, Char            'poke it to display memory
  3272.   WHILE LEN(INKEY$) = 0: WEND   'pause for a keypress
  3273.   POKE Address + 1, Colr        'now poke the color
  3274.   Address = Address + 2         'bump to the next address
  3275.   WHILE LEN(INKEY$) = 0: WEND   'pause for a keypress
  3276. NEXT
  3277. END
  3278.  
  3279. The initial CLS command stores blank spaces and the current BASIC color
  3280. settings in every memory address pair.  Assuming you have not changed the
  3281. color previously, a character value of 32 is stored by CLS into every even
  3282. address, and a color value of 7 in every odd one.  Once the correct video
  3283. segment is known and assigned using DEF SEG, a simple loop pokes each
  3284. character in the string to the display starting at address 0.  (Since
  3285. Address was never assigned initially, it holds a value of zero.)
  3286.      Saving and loading graphics images is of necessity somewhat more
  3287. complex, because you need to know not only the appropriate segment from
  3288. which to save, but also how many bytes.  The example program below creates
  3289. a simple graphic image in CGA screen mode 1, saves the image, and then
  3290. after clearing the screen loads it again.
  3291.  
  3292. DEFINT A-Z
  3293. SCREEN 1
  3294.  
  3295. DEF SEG = 0
  3296. PageSize = PEEK(&H44C) + 256 * PEEK(&H44D)
  3297.  
  3298. FOR X = 1 TO 10
  3299.   CIRCLE (140, 95), X * 10, 2
  3300. NEXT
  3301.  
  3302. DEF SEG = &HB800
  3303. BSAVE "CIRCLES.CGA", 0, PageSize
  3304. PRINT "The screen was just saved, press a key."
  3305. WHILE LEN(INKEY$) = 0: WEND
  3306.  
  3307. CLS
  3308. PRINT "Now press a key to load the screen."
  3309. WHILE LEN(INKEY$) = 0: WEND
  3310. BLOAD "CIRCLES.CGA", 0  
  3311.  
  3312. Notice the use of PEEK to retrieve the current video page size at addresses
  3313. &H44C and &H44D.  This is a handy value that the BIOS maintains in low
  3314. memory, and it tells you how many bytes are occupied by the screen whatever
  3315. its current mode.  In truth, this value is often slightly higher than the
  3316. actual screen dimensions would indicate, since it is rounded up to the next
  3317. even video page boundary.  For example, the 320 by 200 screen mode used
  3318. here occupies 16000 bytes of display memory, yet the page size is reported
  3319. as 16384.  But this value is needed to calculate the appropriate address
  3320. when saving video pages other than page 0.  That is, page 0 begins at
  3321. address 0 at segment &HB800, and page 1 begins at address 16384.
  3322.      Note that many early CGA video adapters contain only 16K of memory,
  3323. and thus do not support multiple screen pages.  Also note that there is a
  3324. small quirk in Hercules adapters that causes the page size to always be
  3325. reported as 16384, even when the screen is in text mode.  I have found this
  3326. word to be unreliable in the EGA and VGA graphics mode.
  3327.      Although you might think that the pixels on a CGA graphics screen
  3328. occupy contiguous memory addresses, they do not.  Although each horizontal
  3329. line is in fact contiguous, the lines are interlaced.  Running the short
  3330. program below shows how the first half of the video addresses contains the
  3331. even rows (starting at row zero), and the second half holds the odd rows.
  3332.  
  3333.  
  3334. SCREEN 1
  3335. DEF SEG = &HB800
  3336. FOR X = 1 TO 15999
  3337.   POKE X, 255
  3338. NEXT
  3339.  
  3340.  
  3341. EGA and VGA displays add yet another level of complexity, because they use
  3342. a separate video memory *plane* to store each color.  Four planes are used
  3343. for EGA and VGA, with one each to hold the red, blue, green, and intensity
  3344. (brightness) information.  Each plane is identified using the same segment
  3345. and address, and OUT instructions are needed to select which is to be made
  3346. currently active.  This is called *bank switching*, because multiple,
  3347. parallel banks of memory are switched in and out of the CPU's address
  3348. space.  When the red plane is active, reading and writing those memory
  3349. locations affects only the red information on the screen.  And when the
  3350. intensity plane is made active, only the brightness for a given pixel on
  3351. the screen is considered.
  3352.      Bank switching is needed to accommodate the enormous amount of
  3353. information that an EGA or VGA screen can contain.  For example, in EGA
  3354. screen mode 9, each plane occupies 28,000 bytes, for a total of 112,000
  3355. bytes of memory.  This far exceeds the amount of memory the designers of
  3356. the original IBM PC anticipated would ever be needed for display purposes. 
  3357. There simply aren't enough addresses available in the PC for video use. 
  3358. Therefore, the only way to deal with that much information is to provide
  3359. additional memory in the EGA and VGA adapters themselves.  When a program
  3360. needs to access a memory plane, it must do that one bank at a time so it
  3361. can be read or written by the CPU.
  3362.      The program below expands slightly on the earlier example, and shows
  3363. how to save and load EGA and VGA screens by manipulating each video plane
  3364. individually.
  3365.  
  3366. DEFINT A-Z
  3367. DECLARE SUB EgaBSave (FileName$)
  3368. DECLARE SUB EgaBLoad (FileName$)
  3369.  
  3370. SCREEN 9
  3371. LOCATE 25, 1
  3372. PRINT "Press a key to stop, and save the screen.";
  3373.  
  3374. '---- clever video effects by Brian Giedt
  3375. WHILE LEN(INKEY$) = 0
  3376.   T = (T MOD 150) + 1
  3377.   C = (C + 1) MOD 16
  3378.   LINE (T, T)-(300 - T, 300 - T), C, B
  3379.   LINE (300 + T, T)-(600 - T, 300 - T), C, B
  3380. WEND
  3381.  
  3382. LOCATE 25, 1
  3383. PRINT "Thank You!"; TAB(75);
  3384. CALL EgaBSave("SCREEN9")
  3385.  
  3386. CLS
  3387. LOCATE 25, 1
  3388. PRINT "Now press a key to read the screen.";
  3389. WHILE LEN(INKEY$) = 0: WEND
  3390. LOCATE 25, 1
  3391. PRINT TAB(75);
  3392.  
  3393. CALL EgaBLoad("SCREEN9")
  3394.  
  3395. SUB EgaBLoad (FileName$) STATIC
  3396.  
  3397.     'UnREM the KILL statements to erase the saved images after they
  3398.     ' have been loaded.
  3399.  
  3400.     DEF SEG = &HA000
  3401.     OUT &H3C4, 2: OUT &H3C5, 1
  3402.     BLOAD FileName$ + ".BLU", 0
  3403.     'KILL FileName$ + ".BLU"
  3404.  
  3405.     OUT &H3C4, 2: OUT &H3C5, 2
  3406.     BLOAD FileName$ + ".GRN", 0
  3407.     'KILL FileName$ + ".GRN"
  3408.  
  3409.     OUT &H3C4, 2: OUT &H3C5, 4
  3410.     BLOAD FileName$ + ".RED", 0
  3411.     'KILL FileName$ + ".RED"
  3412.  
  3413.     OUT &H3C4, 2: OUT &H3C5, 8
  3414.     BLOAD FileName$ + ".INT", 0
  3415.     'KILL FileName$ + ".INT"
  3416.     OUT &H3C4, 2: OUT &H3C5, 15
  3417.  
  3418. END SUB
  3419.  
  3420. SUB EgaBSave (FileName$) STATIC
  3421.  
  3422.     DEF SEG = &HA000
  3423.     Size& = 28000       'use 38400 for VGA SCREEN 12
  3424.  
  3425.     OUT &H3CE, 4: OUT &H3CF, 0
  3426.     BSAVE FileName$ + ".BLU", 0, Size&
  3427.  
  3428.     OUT &H3CE, 4: OUT &H3CF, 1
  3429.     BSAVE FileName$ + ".GRN", 0, Size&
  3430.  
  3431.     OUT &H3CE, 4: OUT &H3CF, 2
  3432.     BSAVE FileName$ + ".RED", 0, Size&
  3433.  
  3434.     OUT &H3CE, 4: OUT &H3CF, 3
  3435.     BSAVE FileName$ + ".INT", 0, Size&
  3436.  
  3437.     OUT &H3CE, 4: OUT &H3CF, 0
  3438.  
  3439. END SUB
  3440.  
  3441. In the EGABLoad and EGABSave subroutines, two OUT statements are actually
  3442. needed to switch planes.  The first gets the EGA adapter's attention, to
  3443. tell it that a subsequent byte is coming.  That second value then indicates
  3444. which memory plane to make currently available.
  3445.  
  3446.  
  3447. THE KEYBOARD DEVICE
  3448.  
  3449. The last device to consider is the keyboard.  BASIC offers several commands
  3450. and functions for accessing the keyboard, and these are INPUT, LINE INPUT,
  3451. INPUT$, and INKEY$.  Further, the "KYBD:" device may be opened as a file,
  3452. and read using the file versions of the first three statements.
  3453.      As with the file versions, INPUT reads numbers or text up to a
  3454. terminating comma or Enter character.  LINE INPUT is for strings only, and
  3455. it ignores commas and requires Enter to be pressed to indicate the end of
  3456. the line.  INPUT$ waits until the specified number of characters have been
  3457. typed before returning, without regard to what characters are entered. 
  3458. INKEY$ returns to the program immediately, even if no key was pressed.
  3459.      Few serious programmers ever use INPUT or LINE INPUT for accepting
  3460. entire lines of text, unless the program is very primitive or will be used
  3461. only occasionally.  The major problem with INPUT and LINE INPUT is that
  3462. there's no way to control how many characters the operator enters.  Once
  3463. you use INPUT or LINE INPUT, you have lost control entirely until the user
  3464. presses Enter.  Worse, when INPUT is used to enter numeric variables, an
  3465. erroneous entry causes BASIC to print its infamous "Redo from start"
  3466. message.  Either of these can spoil the appearance of a carefully designed
  3467. data entry screen.
  3468.      Therefore, the only reasonable way to accept user input is to use
  3469. INKEY$ to read the keys one by one, and act on them individually.  If a
  3470. character key is pressed, the cursor is advanced and the character is added
  3471. to the string.  If the back space key is detected, the cursor is moved to
  3472. the left one column and the current character is erased.  A series of IF or
  3473. CASE statements is often used for this purpose, to handle every key that
  3474. needs to be recognized.
  3475.      The Editor input routine below provides exactly this service, and also
  3476. allows tells you how editing was terminated.  Besides being able to control
  3477. the size of the input editing field, Editor also handles the Insert and
  3478. Delete keys, and recognizes Home and End to jump the beginning and end of
  3479. the field.  A single COLOR statements lets you control the editing field
  3480. color independently of the rest of the screen.  The first portion of the
  3481. code shows how Editor is set up and called.
  3482.  
  3483. DEFINT A-Z
  3484. DECLARE SUB Editor (Text$, LeftCol, RightCol, KeyCode)
  3485.  
  3486. COLOR 7, 1                      'clear to white on blue
  3487. CLS
  3488.  
  3489. Text$ = "This is a test"        'make some sample text
  3490. LeftCol = 20                    'set the left column
  3491. RightCol = 60                   'and the right column
  3492. LOCATE 10                       'set the line number
  3493. COLOR 0, 7                      'set the field color
  3494.  
  3495. DO                              'edit until Enter or Esc
  3496.    CALL Editor(Text$, LeftCol, RightCol, KeyCode)
  3497. LOOP UNTIL KeyCode = 13 OR KeyCode = 27
  3498.  
  3499. SUB Editor (Text$, LeftCol, RightCol, KeyCode)
  3500.  
  3501.   '----- Find the cursor's size.
  3502.   DEF SEG = 0
  3503.   IF PEEK(&H463) = &HB4 THEN
  3504.      CsrSize = 12               'mono uses 13 scan lines
  3505.   ELSE
  3506.      CsrSize = 7                'color uses 8
  3507.   END IF
  3508.  
  3509.   '----- Work with a temporary copy.
  3510.   Edit$ = SPACE$(RightCol - LeftCol + 1)
  3511.   LSET Edit$ = Text$
  3512.  
  3513.   '----- See where to begin editing and print the string.
  3514.   TxtPos = POS(0) - LeftCol + 1
  3515.   IF TxtPos < 1 THEN TxtPos = 1
  3516.   IF TxtPos > LEN(Edit$) THEN TxtPos = LEN(Edit$)
  3517.  
  3518.   LOCATE , LeftCol
  3519.   PRINT Edit$;
  3520.  
  3521.   '----- This is the main loop for handling key presses.
  3522.   DO
  3523.      LOCATE , LeftCol + TxtPos - 1, 1
  3524.  
  3525.      DO
  3526.        Ky$ = INKEY$
  3527.      LOOP UNTIL LEN(Ky$)        'wait for a keypress
  3528.  
  3529.      IF LEN(Ky$) = 1 THEN       'create a key code
  3530.        KeyCode = ASC(Ky$)       'regular character key
  3531.      ELSE                       'extended key
  3532.        KeyCode = -ASC(RIGHT$(Ky$, 1))
  3533.      END IF
  3534.  
  3535.      '----- Branch according to the key pressed.
  3536.      SELECT CASE KeyCode
  3537.  
  3538.        '----- Backspace: decrement the pointer and the
  3539.        '      cursor, but ignore if in the first column.
  3540.        CASE 8
  3541.          TxtPos = TxtPos - 1
  3542.          LOCATE , LeftCol + TxtPos - 1, 0
  3543.          IF TxtPos > 0 THEN
  3544.            IF Insert THEN
  3545.              MID$(Edit$, TxtPos) = MID$(Edit$, TxtPos + 1) + " "
  3546.            ELSE
  3547.              MID$(Edit$, TxtPos) = " "
  3548.            END IF
  3549.              PRINT MID$(Edit$, TxtPos);
  3550.          END IF
  3551.  
  3552.        '----- Enter or Escape: this block is optional in
  3553.        '      case you want to handle these separately.
  3554.        CASE 13, 27
  3555.          EXIT DO                'exit the subprogram
  3556.  
  3557.        '----- Letter keys: turn off the cursor to hide
  3558.        '      the printing, handle Insert mode as needed.
  3559.        CASE 32 TO 254
  3560.          LOCATE , , 0
  3561.          IF Insert THEN         'expand the string
  3562.            MID$(Edit$, TxtPos) = Ky$ + MID$(Edit$, TxtPos)
  3563.            PRINT MID$(Edit$, TxtPos);
  3564.          ELSE                   'else insert character
  3565.            MID$(Edit$, TxtPos) = Ky$
  3566.            PRINT Ky$;
  3567.          END IF
  3568.          TxtPos = TxtPos + 1    'update position counter
  3569.  
  3570.        '----- Left arrow: decrement the position counter.
  3571.        CASE -75
  3572.          TxtPos = TxtPos - 1
  3573.  
  3574.        '----- Right arrow: increment position counter.
  3575.        CASE -77
  3576.          TxtPos = TxtPos + 1
  3577.  
  3578.        '----- Home: jump to the first character position.
  3579.        CASE -71
  3580.          TxtPos = 1
  3581.  
  3582.        '----- End: search for the last non-blank, and
  3583.        '      make that the current editing position.
  3584.        CASE -79
  3585.          FOR N = LEN(Edit$) TO 1 STEP -1
  3586.            IF MID$(Edit$, N, 1) <> " " THEN EXIT FOR
  3587.          NEXT
  3588.          TxtPos = N + 1
  3589.          IF TxtPos > LEN(Edit$) THEN TxtPos = LEN(Edit$)
  3590.  
  3591.        '----- Insert key: toggle the Insert state and
  3592.        '      adjust the cursor size.
  3593.        CASE -82
  3594.          Insert = NOT Insert
  3595.          IF Insert THEN
  3596.            LOCATE , , , CsrSize \ 2, CsrSize
  3597.          ELSE
  3598.            LOCATE , , , CsrSize - 1, CsrSize
  3599.          END IF
  3600.  
  3601.        '----- Delete: delete the current character and
  3602.        '      reprint what remains in the string.
  3603.        CASE -83
  3604.          MID$(Edit$, TxtPos) = MID$(Edit$, TxtPos + 1) + " "
  3605.          LOCATE , , 0
  3606.          PRINT MID$(Edit$, TxtPos);
  3607.  
  3608.        '---- All other keys: exit the subprogram
  3609.        CASE ELSE
  3610.          EXIT DO
  3611.      END SELECT
  3612.  
  3613.   '----- Loop until the cursor moves out of the field.
  3614.   LOOP UNTIL TxtPos < 1 OR TxtPos > LEN(Edit$)
  3615.  
  3616.   Text$ = RTRIM$(Edit$)         'trim the text
  3617.  
  3618. END SUB
  3619.  
  3620. Most of the details in this subprogram do not require much explanation, and
  3621. the code should prove simple enough to be self-documenting.  However, I
  3622. would like to discuss INKEY$ as it is used here.
  3623.      Each time INKEY$ is used it examines the keyboard buffer, to see if a
  3624. key is pending.  If not, a null string is returned.  If a key is present in
  3625. the buffer INKEY$ removes it, and returns either a 1- or 2-byte string,
  3626. depending on what type of key it is.  Normal character keys and control
  3627. keys (entered by pressing the Ctrl key in conjunction with a regular key)
  3628. are returned as a 1-byte string.  Some special keys such as Enter and
  3629. Escape are also returned as a 1-byte string, because they are in fact
  3630. control keys.  For example, Enter is the same as Ctrl-M, and Escape is
  3631. identical to the Ctrl-[ key.
  3632.      The IBM PC offers additional keys and key combinations that are not
  3633. defined by the ASCII standard, and these are returned as a 2-byte string so
  3634. your program can identify them.  Extended keys include the function keys,
  3635. Home and End and the other cursor control keys, and Alt key combinations. 
  3636. When an extended key is returned the first character is always CHR$(0), and
  3637. the second character corresponds to the extended key's code using a method
  3638. defined by IBM.  Therefore, you can determine if a key is extended either
  3639. by looking for a length of two, or by examining the first character to see
  3640. if it is a CHR$(0) zero byte.
  3641.      There are three ways to accomplish this, and which is best depends on
  3642. the compiler you are using.  The brief program fragment below shows each
  3643. method, and the number of bytes that are generated by both compilers.
  3644.  
  3645.  
  3646. IF LEN(X$) = 2 THEN             '17 for QB4, 7 for PDS
  3647.  
  3648. IF ASC(X$) THEN                 '16 for QB4, 13 for PDS
  3649.  
  3650. IF LEFT$(X$, 1) = CHR$(0) THEN  '33 for QB4, 30 for PDS
  3651.  
  3652.  
  3653. The references to QB 4 are valid for both QuickBASIC 4.0 and 4.5.  The
  3654. BASIC PDS byte counts reflect that compiler's improved code optimization,
  3655. however this improvement is available only with near strings.  When far
  3656. strings are used the LEN test requires the same 13 bytes as the ASC test.
  3657. [I'll presume that VB/DOS, with its support for only far strings, also uses
  3658. the longer byte count.]
  3659.      As you can see, the test that uses BASIC's ASC function is slightly
  3660. better than the one that uses LEN if you are using QuickBASIC.  But if you
  3661. have BASIC PDS the LEN test is quite a bit shorter.  Comparing the first
  3662. character in the string is much worse for either compiler, because
  3663. individual calls must be made to BASIC's LEFT$, CHR$, and string comparison
  3664. routines.
  3665.      Even though the length and address of a QuickBASIC string is stored in
  3666. the string's descriptor and is easily available to the compiler, the BC
  3667. compiler that comes with QuickBASIC still calls a LEN routine.  Where the
  3668. compiler *could* use CMP WORD PTR [DescriptorAddress], 2 to see if the
  3669. string length is 2, it instead passes the address of the string descriptor
  3670. on the stack, calls the LEN routine, and compares the result LEN returns. 
  3671. Fortunately, this optimization was added in BASIC PDS when near strings are
  3672. used.  Likewise, SADD when used with PDS near strings directly retrieves
  3673. the string's address from the descriptor as well, instead of calling a
  3674. library routine as QuickBASIC does.
  3675.      The Editor subprogram uses the LEN method to determine the type of key
  3676. that was pressed, which is most efficient if you are using BASIC PDS. 
  3677. Because integer comparisons are faster and generate less code than the
  3678. equivalent operation with strings, ASC is then used to obtain either the
  3679. ASCII value of the key, or the value of the extended key code.  The result
  3680. is assigned to the variable KeyCode as either a positive number to indicate
  3681. a regular ASCII key, or a negative value that corresponds to an extended
  3682. key's code.  This method helps to reduce the size of the subprogram, by
  3683. eliminating string comparisons in each CASE statement.
  3684.      One important warning when using ASC is that it will generate an
  3685. "Illegal function call" error if you pass it a null string.  Therefore, in
  3686. many cases you must include an additional test just for that:
  3687.  
  3688.  
  3689. IF LEN(Work$) THEN
  3690.   IF ASC(Work$) THEN
  3691.     ...
  3692.     ...
  3693.   END IF
  3694. END IF
  3695.  
  3696.  
  3697. One solution is to create your own function--perhaps called ASCII%()--that
  3698. does this for you.  Since calling a BASIC function requires no more code
  3699. than when BASIC calls its own routines (assuming you are using the same
  3700. number of arguments, of course), this can also help to reduce the size of
  3701. your programs.  I like to use a return value of -1 to indicate a null
  3702. string, as shown below.
  3703.  
  3704.  
  3705. FUNCTION ASCII%(This$)
  3706.   IF LEN(This$) THEN
  3707.     ASCII% = ASC(This$)
  3708.   ELSE
  3709.     ASCII% = -1
  3710.   END IF
  3711. END FUNCTION
  3712.  
  3713.  
  3714. Now you can simply use code such as IF ASCII%(Any$) = Whatever THEN...
  3715. confident that no error will occur and the returned value will still be
  3716. valid.
  3717.  
  3718.  
  3719. Redirection
  3720.  
  3721. One clever DOS feature that many programmers are not aware of is its
  3722. ability to redirect a program's normal input and output to a file.  When a
  3723. program is redirected, print statements go to a specified file, keyboard
  3724. input is read from a file, or both.  The actual redirection commands are
  3725. entered by the user of your program, and your program has no idea that this
  3726. has happened.  This is really more a DOS issue than a BASIC concern, but
  3727. it's a powerful feature and you should understand how it works.
  3728.      Redirection is useful for capturing a program's output to a disk file,
  3729. or feeding keystrokes to a program using a predefined sequence contained in
  3730. a file.  For example, the output of the DOS DIR command can be redirected
  3731. to a file with this command:
  3732.  
  3733.      dir *.* > anyfile
  3734.  
  3735. Redirecting a program's input can be equally valuable.  If you often format
  3736. several diskettes at once you might create a file that contains the answer
  3737. Y followed by an Enter character, and then run format using this:
  3738.  
  3739.      format < yesfile
  3740.  
  3741. This way the file will provide the response to "Format another (Y/N)?".
  3742.      To redirect a program's output, start it from the DOS command line and
  3743. place a *greater than* symbol and the output file name at the end of the
  3744. command line:
  3745.  
  3746.      program > filename
  3747.  
  3748. Similarly, using a *less than* sign tells DOS to replace the program's
  3749. requests for keyboard input with the contents of the specified file, thus:
  3750.  
  3751.      program < filename
  3752.  
  3753. You can combine both redirected input and output at the same time, and the
  3754. order in which they are given does not matter.  It is important to
  3755. understand that redirecting a program's output to a file is similar to
  3756. opening that file for output.  That is, it is created if it didn't yet
  3757. exist, or truncated to a length of zero if it did.  However, DOS also lets
  3758. you append to a file when redirecting output, using two symbols in a row:
  3759.  
  3760.      program >> filename
  3761.  
  3762. Please be aware that you can hang a PC completely when redirecting a
  3763. program's input, if the necessary characters are not present.  For example,
  3764. this would happen when redirecting a program that uses LINE INPUT from a
  3765. file that has no terminating CHR$(13) Enter character.  Even pressing Ctrl-
  3766. Break will have no effect, and your only recourse is to reboot, or close
  3767. down the DOS session if you are using Windows.
  3768.  
  3769.  
  3770. SUMMARY
  3771. =======
  3772.  
  3773. This chapter has presented an enormous amount of information about both
  3774. files and devices in BASIC.  If began with a brief overview of how DOS
  3775. allocates disk storage using sectors and clusters, and continued with an
  3776. explanation of file buffers.  By understanding the relationship between
  3777. BASIC's own buffers and their impact on string memory, you gain greater
  3778. control over your program's speed and memory requirements.
  3779.      This then led to a comparison of files and devices, and showed how
  3780. they can be controlled by similar BASIC statements.  In particular, you
  3781. learned how the same block of code can be used to send information to
  3782. either, simplifying the design of reports and other programming output
  3783. chores.
  3784.      The section that described file access methods compared all of the
  3785. available options, and explained when each is appropriate and why.  You
  3786. learned that all DOS files are really just a continuous stream of binary
  3787. data, and the various OPEN methods merely let you indicate to BASIC how
  3788. that data is to be handled.
  3789.      You also learned that the best way to improve a program's file access
  3790. speed is to read and write data in large blocks.  Several complete
  3791. subprograms and functions were shown to illustrate this technique, and most
  3792. are general enough to be useful when included within your own programs.
  3793.      Numerous tips and tricks were presented to determine the type of
  3794. display adapter installed, run .COM programs and .BAT files, obtain
  3795. formatted numbers by combining PRINT USING # with FIELD and INPUT #, and
  3796. many more.  You were also introduced to the possibility of calling BASIC's
  3797. internal library routines as a way to circumvent many otherwise arbitrary
  3798. limitations in the language.
  3799.      Finally, video memory organization was revealed for all of the popular
  3800. screen modes, and example programs were provided to show how they may be
  3801. saved and loaded.
  3802.      In the next chapter I will continue this discussion of files with
  3803. detailed explanations of writing database programs.  Chapter 7 will also
  3804. describe how to write programs that operate on a network, as well as how to
  3805. access data that uses the popular dBASE file format.
  3806.